diff --git a/.ci/Dockerfile b/.ci/Dockerfile deleted file mode 100644 index 201e17b93c116..0000000000000 --- a/.ci/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -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/runbld_no_junit.yml b/.ci/runbld_no_junit.yml index 1bcb7e22a2648..67b5002c1c437 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: false + junit-filename-pattern: "8d8bd494-d909-4e67-a052-7e8b5aaeb5e4" # A bogus path that should never exist diff --git a/.gitignore b/.gitignore index 25a8c369bb704..32377ec0f1ffe 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,6 @@ 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 491a1e386deb1..f6f77ccae8427 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,7 +8,50 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) ciStats.trackBuild { catchError { retryable.enable() - kibanaPipeline.allCiTasks() + 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-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'), + 'xpack-securitySolutionCypress': { processNumber -> + whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { + kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) + } + }, + + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) } } } diff --git a/docs/images/canvas-add-image.gif b/docs/canvas/images/canvas-add-image.gif similarity index 100% rename from docs/images/canvas-add-image.gif rename to docs/canvas/images/canvas-add-image.gif diff --git a/docs/images/canvas-add-pages.gif b/docs/canvas/images/canvas-add-pages.gif similarity index 100% rename from docs/images/canvas-add-pages.gif rename to docs/canvas/images/canvas-add-pages.gif diff --git a/docs/images/canvas-autoplay-interval.png b/docs/canvas/images/canvas-autoplay-interval.png similarity index 100% rename from docs/images/canvas-autoplay-interval.png rename to docs/canvas/images/canvas-autoplay-interval.png diff --git a/docs/images/canvas-background-color-picker.png b/docs/canvas/images/canvas-background-color-picker.png similarity index 100% rename from docs/images/canvas-background-color-picker.png rename to docs/canvas/images/canvas-background-color-picker.png diff --git a/docs/images/canvas-change-your-expression-chart-no-legend.png b/docs/canvas/images/canvas-change-your-expression-chart-no-legend.png similarity index 100% rename from docs/images/canvas-change-your-expression-chart-no-legend.png rename to docs/canvas/images/canvas-change-your-expression-chart-no-legend.png diff --git a/docs/images/canvas-change-your-expression-chart.png b/docs/canvas/images/canvas-change-your-expression-chart.png similarity index 100% rename from docs/images/canvas-change-your-expression-chart.png rename to docs/canvas/images/canvas-change-your-expression-chart.png diff --git a/docs/images/canvas-chart-element.png b/docs/canvas/images/canvas-chart-element.png similarity index 100% rename from docs/images/canvas-chart-element.png rename to docs/canvas/images/canvas-chart-element.png diff --git a/docs/images/canvas-create-URL.gif b/docs/canvas/images/canvas-create-URL.gif similarity index 100% rename from docs/images/canvas-create-URL.gif rename to docs/canvas/images/canvas-create-URL.gif diff --git a/docs/images/canvas-element-select.gif b/docs/canvas/images/canvas-element-select.gif similarity index 100% rename from docs/images/canvas-element-select.gif rename to docs/canvas/images/canvas-element-select.gif diff --git a/docs/images/canvas-export-workpad.png b/docs/canvas/images/canvas-export-workpad.png similarity index 100% rename from docs/images/canvas-export-workpad.png rename to docs/canvas/images/canvas-export-workpad.png diff --git a/docs/images/canvas-fullscreen.png b/docs/canvas/images/canvas-fullscreen.png similarity index 100% rename from docs/images/canvas-fullscreen.png rename to docs/canvas/images/canvas-fullscreen.png diff --git a/docs/images/canvas-functions-can-take-arguments-donut-chart.png b/docs/canvas/images/canvas-functions-can-take-arguments-donut-chart.png similarity index 100% rename from docs/images/canvas-functions-can-take-arguments-donut-chart.png rename to docs/canvas/images/canvas-functions-can-take-arguments-donut-chart.png diff --git a/docs/images/canvas-functions-can-take-arguments-pie-chart.png b/docs/canvas/images/canvas-functions-can-take-arguments-pie-chart.png similarity index 100% rename from docs/images/canvas-functions-can-take-arguments-pie-chart.png rename to docs/canvas/images/canvas-functions-can-take-arguments-pie-chart.png diff --git a/docs/images/canvas-generate-pdf.gif b/docs/canvas/images/canvas-generate-pdf.gif similarity index 100% rename from docs/images/canvas-generate-pdf.gif rename to docs/canvas/images/canvas-generate-pdf.gif diff --git a/docs/images/canvas-gs-example.png b/docs/canvas/images/canvas-gs-example.png similarity index 100% rename from docs/images/canvas-gs-example.png rename to docs/canvas/images/canvas-gs-example.png diff --git a/docs/images/canvas-image-element.png b/docs/canvas/images/canvas-image-element.png similarity index 100% rename from docs/images/canvas-image-element.png rename to docs/canvas/images/canvas-image-element.png diff --git a/docs/images/canvas-map-embed.gif b/docs/canvas/images/canvas-map-embed.gif similarity index 100% rename from docs/images/canvas-map-embed.gif rename to docs/canvas/images/canvas-map-embed.gif diff --git a/docs/images/canvas-metric-element.png b/docs/canvas/images/canvas-metric-element.png similarity index 100% rename from docs/images/canvas-metric-element.png rename to docs/canvas/images/canvas-metric-element.png diff --git a/docs/images/canvas-refresh-interval.png b/docs/canvas/images/canvas-refresh-interval.png similarity index 100% rename from docs/images/canvas-refresh-interval.png rename to docs/canvas/images/canvas-refresh-interval.png diff --git a/docs/images/canvas-timefilter-element.png b/docs/canvas/images/canvas-timefilter-element.png similarity index 100% rename from docs/images/canvas-timefilter-element.png rename to docs/canvas/images/canvas-timefilter-element.png diff --git a/docs/images/canvas-zoom-controls.png b/docs/canvas/images/canvas-zoom-controls.png similarity index 100% rename from docs/images/canvas-zoom-controls.png rename to docs/canvas/images/canvas-zoom-controls.png diff --git a/docs/images/canvas_element_options.png b/docs/canvas/images/canvas_element_options.png similarity index 100% rename from docs/images/canvas_element_options.png rename to docs/canvas/images/canvas_element_options.png diff --git a/docs/images/canvas_save_element.png b/docs/canvas/images/canvas_save_element.png similarity index 100% rename from docs/images/canvas_save_element.png rename to docs/canvas/images/canvas_save_element.png diff --git a/docs/images/settings.png b/docs/dev-tools/console/images/settings.png similarity index 100% rename from docs/images/settings.png rename to docs/dev-tools/console/images/settings.png diff --git a/docs/images/jenkins/job_view.png b/docs/developer/images/job_view.png similarity index 100% rename from docs/images/jenkins/job_view.png rename to docs/developer/images/job_view.png diff --git a/docs/images/jenkins/pipeline_steps_view.png b/docs/developer/images/pipeline_steps_view.png similarity index 100% rename from docs/images/jenkins/pipeline_steps_view.png rename to docs/developer/images/pipeline_steps_view.png diff --git a/docs/developer/testing/interpreting-ci-failures.asciidoc b/docs/developer/testing/interpreting-ci-failures.asciidoc index bc237928cf5aa..c47a59217d89b 100644 --- a/docs/developer/testing/interpreting-ci-failures.asciidoc +++ b/docs/developer/testing/interpreting-ci-failures.asciidoc @@ -17,7 +17,7 @@ Clicking the link next to the check in the conversation tab of a pull request wi To view the results of a job execution in Jenkins, either click the link in the comment left by `@elasticmachine` or search for the `kibana-ci` check in the list at the bottom of the PR. This link will take you to the top-level page for the specific job execution that failed. -image::images/jenkins/job_view.png[] +image::images/job_view.png[] 1. *Git Changes:* the list of commits that were in this build which weren't in the previous build. For Pull Requests this list is calculated by comparing against the most recent Pull Request which was tested, it is not limited to build for this specific Pull Request, so it's not very useful. 2. *Test Results:* A link to the test results screen, and shortcuts to the failed tests. Functional tests capture and store the log output from each specific test, and make it visible at these links. For other test runners only the error message is visible and log output must be tracked down in the *Pipeline Steps*. @@ -29,6 +29,6 @@ image::images/jenkins/job_view.png[] To view the logs for a failed specific ciGroup, jest, mocha, type checkers, linters, etc., click on the *Pipeline Steps* link in from the Job page. -image::images/jenkins/pipeline_steps_view.png[] +image::images/pipeline_steps_view.png[] Scroll down the page until you find a failed step *(1)*, and then look up a few lines for the `Branch:` step to see which specific job this is. If this is the job you're looking for click the little terminal icon next to the failed step *(1)* to view the logs for that specific step in the Pipeline. \ No newline at end of file diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 7ebd0531619fd..8d4c0c915437e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -154,7 +154,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsBulkUpdateResponse](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | -| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation.Note: this type intentially doesn't include a type definition for defining the dynamic mapping parameter. Saved Object fields should always inherit the dynamic: 'strict' paramater. If you are unsure of the shape of your data use type: 'object', enabled: false instead. | +| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md new file mode 100644 index 0000000000000..b01da3c62fda6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) + +## SavedObjectsComplexFieldMapping.dynamic property + +The dynamic property of the mapping, either `false` or `'strict'`. If unspecified `dynamic: 'strict'` will be inherited from the top-level index mappings. + +Note: To limit the number of mapping fields Saved Object types should \*never\* use `dynamic: true`. + +Signature: + +```typescript +dynamic?: false | 'strict'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md new file mode 100644 index 0000000000000..08513aa2a849b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [enabled](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md) + +## SavedObjectsComplexFieldMapping.enabled property + +Signature: + +```typescript +enabled?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md index cb81686b424ec..fc262cad54f18 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md @@ -6,8 +6,6 @@ See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. -Note: this type intentially doesn't include a type definition for defining the `dynamic` mapping parameter. Saved Object fields should always inherit the `dynamic: 'strict'` paramater. If you are unsure of the shape of your data use `type: 'object', enabled: false` instead. - Signature: ```typescript @@ -19,6 +17,8 @@ export interface SavedObjectsComplexFieldMapping | Property | Type | Description | | --- | --- | --- | | [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md) | boolean | | +| [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) | false | 'strict' | The dynamic property of the mapping, either false or 'strict'. If unspecified dynamic: 'strict' will be inherited from the top-level index mappings.Note: To limit the number of mapping fields Saved Object types should \*never\* use dynamic: true. | +| [enabled](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md) | boolean | | | [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties | | | [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md deleted file mode 100644 index c0b556e99ebc3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) - -## SavedObjectsCoreFieldMapping.enabled property - -Signature: - -```typescript -enabled?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md index b9e726eac799d..e9b9c2bcf51b5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md @@ -17,7 +17,6 @@ export interface SavedObjectsCoreFieldMapping | Property | Type | Description | | --- | --- | --- | | [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md) | boolean | | -| [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean | | | [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
ignore_above?: number;
};
} | | | [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean | | | [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) | number | boolean | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md index 74efa75768f9c..70775760ac77d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md @@ -4,7 +4,7 @@ ## SavedObjectsTypeMappingDefinition.dynamic property -The dynamic property of the mapping. either `false` or 'strict'. Defaults to `false` +The dynamic property of the mapping, either `false` or `'strict'`. If unspecified `dynamic: 'strict'` will be inherited from the top-level index mappings. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md index 77ded4389c0a0..3d3b73880fa7f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md @@ -41,6 +41,6 @@ const typeDefinition: SavedObjectsTypeMappingDefinition = { | Property | Type | Description | | --- | --- | --- | -| [dynamic](./kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md) | false | 'strict' | The dynamic property of the mapping. either false or 'strict'. Defaults to false | +| [dynamic](./kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md) | false | 'strict' | The dynamic property of the mapping, either false or 'strict'. If unspecified dynamic: 'strict' will be inherited from the top-level index mappings. | | [properties](./kibana-plugin-core-server.savedobjectstypemappingdefinition.properties.md) | SavedObjectsMappingProperties | The underlying properties of the type mapping | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md index a48f4920b3d26..e515c3513df6c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md @@ -8,32 +8,33 @@ ```typescript UI_SETTINGS: { - META_FIELDS: string; - DOC_HIGHLIGHT: string; - QUERY_STRING_OPTIONS: string; - QUERY_ALLOW_LEADING_WILDCARDS: string; - SEARCH_QUERY_LANGUAGE: string; - SORT_OPTIONS: string; - COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; - COURIER_SET_REQUEST_PREFERENCE: string; - COURIER_CUSTOM_REQUEST_PREFERENCE: string; - COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; - COURIER_BATCH_SEARCHES: string; - SEARCH_INCLUDE_FROZEN: string; - HISTOGRAM_BAR_TARGET: string; - HISTOGRAM_MAX_BARS: string; - HISTORY_LIMIT: string; - SHORT_DOTS_ENABLE: string; - FORMAT_DEFAULT_TYPE_MAP: string; - FORMAT_NUMBER_DEFAULT_PATTERN: string; - FORMAT_PERCENT_DEFAULT_PATTERN: string; - FORMAT_BYTES_DEFAULT_PATTERN: string; - FORMAT_CURRENCY_DEFAULT_PATTERN: string; - FORMAT_NUMBER_DEFAULT_LOCALE: string; - TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; - TIMEPICKER_QUICK_RANGES: string; - INDEXPATTERN_PLACEHOLDER: string; - FILTERS_PINNED_BY_DEFAULT: string; - FILTERS_EDITOR_SUGGEST_VALUES: string; + readonly META_FIELDS: "metaFields"; + readonly DOC_HIGHLIGHT: "doc_table:highlight"; + readonly QUERY_STRING_OPTIONS: "query:queryString:options"; + readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards"; + readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage"; + readonly SORT_OPTIONS: "sort:options"; + readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex"; + readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference"; + readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference"; + readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; + readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; + readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; + readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; + readonly HISTORY_LIMIT: "history:limit"; + readonly SHORT_DOTS_ENABLE: "shortDots:enable"; + readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap"; + readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern"; + readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern"; + readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern"; + readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern"; + readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale"; + readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults"; + readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges"; + readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults"; + readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; + readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; + readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md index 855cfd11d00ea..e419b64cd43aa 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md @@ -8,32 +8,33 @@ ```typescript UI_SETTINGS: { - META_FIELDS: string; - DOC_HIGHLIGHT: string; - QUERY_STRING_OPTIONS: string; - QUERY_ALLOW_LEADING_WILDCARDS: string; - SEARCH_QUERY_LANGUAGE: string; - SORT_OPTIONS: string; - COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; - COURIER_SET_REQUEST_PREFERENCE: string; - COURIER_CUSTOM_REQUEST_PREFERENCE: string; - COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; - COURIER_BATCH_SEARCHES: string; - SEARCH_INCLUDE_FROZEN: string; - HISTOGRAM_BAR_TARGET: string; - HISTOGRAM_MAX_BARS: string; - HISTORY_LIMIT: string; - SHORT_DOTS_ENABLE: string; - FORMAT_DEFAULT_TYPE_MAP: string; - FORMAT_NUMBER_DEFAULT_PATTERN: string; - FORMAT_PERCENT_DEFAULT_PATTERN: string; - FORMAT_BYTES_DEFAULT_PATTERN: string; - FORMAT_CURRENCY_DEFAULT_PATTERN: string; - FORMAT_NUMBER_DEFAULT_LOCALE: string; - TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; - TIMEPICKER_QUICK_RANGES: string; - INDEXPATTERN_PLACEHOLDER: string; - FILTERS_PINNED_BY_DEFAULT: string; - FILTERS_EDITOR_SUGGEST_VALUES: string; + readonly META_FIELDS: "metaFields"; + readonly DOC_HIGHLIGHT: "doc_table:highlight"; + readonly QUERY_STRING_OPTIONS: "query:queryString:options"; + readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards"; + readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage"; + readonly SORT_OPTIONS: "sort:options"; + readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex"; + readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference"; + readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference"; + readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; + readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; + readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; + readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; + readonly HISTORY_LIMIT: "history:limit"; + readonly SHORT_DOTS_ENABLE: "shortDots:enable"; + readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap"; + readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern"; + readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern"; + readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern"; + readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern"; + readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale"; + readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults"; + readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges"; + readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults"; + readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; + readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; + readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; } ``` diff --git a/docs/images/Discover-ContextView.png b/docs/discover/images/Discover-ContextView.png similarity index 100% rename from docs/images/Discover-ContextView.png rename to docs/discover/images/Discover-ContextView.png diff --git a/docs/images/Discover-Start.png b/docs/discover/images/Discover-Start.png similarity index 100% rename from docs/images/Discover-Start.png rename to docs/discover/images/Discover-Start.png diff --git a/docs/images/Expanded-Document.png b/docs/discover/images/Expanded-Document.png similarity index 100% rename from docs/images/Expanded-Document.png rename to docs/discover/images/Expanded-Document.png diff --git a/docs/images/Histogram-Time.png b/docs/discover/images/Histogram-Time.png similarity index 100% rename from docs/images/Histogram-Time.png rename to docs/discover/images/Histogram-Time.png diff --git a/docs/images/NegativeFilter.jpg b/docs/discover/images/NegativeFilter.jpg similarity index 100% rename from docs/images/NegativeFilter.jpg rename to docs/discover/images/NegativeFilter.jpg diff --git a/docs/images/PositiveFilter.jpg b/docs/discover/images/PositiveFilter.jpg similarity index 100% rename from docs/images/PositiveFilter.jpg rename to docs/discover/images/PositiveFilter.jpg diff --git a/docs/images/Timepicker-View.png b/docs/discover/images/Timepicker-View.png similarity index 100% rename from docs/images/Timepicker-View.png rename to docs/discover/images/Timepicker-View.png diff --git a/docs/images/edit_filter_query_json.png b/docs/discover/images/edit_filter_query_json.png similarity index 100% rename from docs/images/edit_filter_query_json.png rename to docs/discover/images/edit_filter_query_json.png diff --git a/docs/images/filter-field.png b/docs/discover/images/filter-field.png similarity index 100% rename from docs/images/filter-field.png rename to docs/discover/images/filter-field.png diff --git a/docs/images/time-filter-bar.png b/docs/discover/images/time-filter-bar.png similarity index 100% rename from docs/images/time-filter-bar.png rename to docs/discover/images/time-filter-bar.png diff --git a/docs/images/time-filter-calendar.png b/docs/discover/images/time-filter-calendar.png similarity index 100% rename from docs/images/time-filter-calendar.png rename to docs/discover/images/time-filter-calendar.png diff --git a/docs/images/tutorial-dashboard.png b/docs/getting-started/images/tutorial-dashboard.png similarity index 100% rename from docs/images/tutorial-dashboard.png rename to docs/getting-started/images/tutorial-dashboard.png diff --git a/docs/images/tutorial-discover-2.png b/docs/getting-started/images/tutorial-discover-2.png similarity index 100% rename from docs/images/tutorial-discover-2.png rename to docs/getting-started/images/tutorial-discover-2.png diff --git a/docs/images/tutorial-discover-3.png b/docs/getting-started/images/tutorial-discover-3.png similarity index 100% rename from docs/images/tutorial-discover-3.png rename to docs/getting-started/images/tutorial-discover-3.png diff --git a/docs/images/tutorial-full-inspect1.png b/docs/getting-started/images/tutorial-full-inspect1.png similarity index 100% rename from docs/images/tutorial-full-inspect1.png rename to docs/getting-started/images/tutorial-full-inspect1.png diff --git a/docs/images/tutorial-pattern-1.png b/docs/getting-started/images/tutorial-pattern-1.png similarity index 100% rename from docs/images/tutorial-pattern-1.png rename to docs/getting-started/images/tutorial-pattern-1.png diff --git a/docs/images/tutorial-visualize-bar-1.5.png b/docs/getting-started/images/tutorial-visualize-bar-1.5.png similarity index 100% rename from docs/images/tutorial-visualize-bar-1.5.png rename to docs/getting-started/images/tutorial-visualize-bar-1.5.png diff --git a/docs/images/tutorial-visualize-map-2.png b/docs/getting-started/images/tutorial-visualize-map-2.png similarity index 100% rename from docs/images/tutorial-visualize-map-2.png rename to docs/getting-started/images/tutorial-visualize-map-2.png diff --git a/docs/images/tutorial-visualize-md-2.png b/docs/getting-started/images/tutorial-visualize-md-2.png similarity index 100% rename from docs/images/tutorial-visualize-md-2.png rename to docs/getting-started/images/tutorial-visualize-md-2.png diff --git a/docs/images/tutorial-visualize-pie-2.png b/docs/getting-started/images/tutorial-visualize-pie-2.png similarity index 100% rename from docs/images/tutorial-visualize-pie-2.png rename to docs/getting-started/images/tutorial-visualize-pie-2.png diff --git a/docs/images/tutorial-visualize-pie-3.png b/docs/getting-started/images/tutorial-visualize-pie-3.png similarity index 100% rename from docs/images/tutorial-visualize-pie-3.png rename to docs/getting-started/images/tutorial-visualize-pie-3.png diff --git a/docs/images/tutorial-visualize-wizard-step-1.png b/docs/getting-started/images/tutorial-visualize-wizard-step-1.png similarity index 100% rename from docs/images/tutorial-visualize-wizard-step-1.png rename to docs/getting-started/images/tutorial-visualize-wizard-step-1.png diff --git a/docs/images/AddFieldButton.jpg b/docs/images/AddFieldButton.jpg deleted file mode 100644 index efd4f50e34a0b..0000000000000 Binary files a/docs/images/AddFieldButton.jpg and /dev/null differ diff --git a/docs/images/CollapseButton.jpg b/docs/images/CollapseButton.jpg deleted file mode 100644 index 38bb350d49746..0000000000000 Binary files a/docs/images/CollapseButton.jpg and /dev/null differ diff --git a/docs/images/Dashboard_Resize_Menu.png b/docs/images/Dashboard_Resize_Menu.png deleted file mode 100644 index 835d23afe40e9..0000000000000 Binary files a/docs/images/Dashboard_Resize_Menu.png and /dev/null differ diff --git a/docs/images/Dashboard_visualization_data.png b/docs/images/Dashboard_visualization_data.png deleted file mode 100644 index 9792fedf1a51a..0000000000000 Binary files a/docs/images/Dashboard_visualization_data.png and /dev/null differ diff --git a/docs/images/Discover-ContextView-FilterMontage.png b/docs/images/Discover-ContextView-FilterMontage.png deleted file mode 100644 index c990d314a6ba1..0000000000000 Binary files a/docs/images/Discover-ContextView-FilterMontage.png and /dev/null differ diff --git a/docs/images/Discover-FieldStats.jpg b/docs/images/Discover-FieldStats.jpg deleted file mode 100644 index 4092b0d7caafd..0000000000000 Binary files a/docs/images/Discover-FieldStats.jpg and /dev/null differ diff --git a/docs/images/Discover-MoveColumn.jpg b/docs/images/Discover-MoveColumn.jpg deleted file mode 100644 index 630f2a0f18dbe..0000000000000 Binary files a/docs/images/Discover-MoveColumn.jpg and /dev/null differ diff --git a/docs/images/EditVis.png b/docs/images/EditVis.png deleted file mode 100644 index 3013168200860..0000000000000 Binary files a/docs/images/EditVis.png and /dev/null differ diff --git a/docs/images/ExistsButton.jpg b/docs/images/ExistsButton.jpg deleted file mode 100644 index 0d4ede0101e73..0000000000000 Binary files a/docs/images/ExistsButton.jpg and /dev/null differ diff --git a/docs/images/ExpandButton.jpg b/docs/images/ExpandButton.jpg deleted file mode 100644 index 1ed389a25dd36..0000000000000 Binary files a/docs/images/ExpandButton.jpg and /dev/null differ diff --git a/docs/images/NYCTA-Table.jpg b/docs/images/NYCTA-Table.jpg deleted file mode 100644 index 6b4987ef4b437..0000000000000 Binary files a/docs/images/NYCTA-Table.jpg and /dev/null differ diff --git a/docs/images/NewDashboard.png b/docs/images/NewDashboard.png deleted file mode 100644 index 08e5159250134..0000000000000 Binary files a/docs/images/NewDashboard.png and /dev/null differ diff --git a/docs/images/RemoveFieldButton.jpg b/docs/images/RemoveFieldButton.jpg deleted file mode 100644 index a260dc3cff62e..0000000000000 Binary files a/docs/images/RemoveFieldButton.jpg and /dev/null differ diff --git a/docs/images/Start-Page.png b/docs/images/Start-Page.png deleted file mode 100644 index 706d4aafd75e2..0000000000000 Binary files a/docs/images/Start-Page.png and /dev/null differ diff --git a/docs/images/TimeFilter.jpg b/docs/images/TimeFilter.jpg deleted file mode 100644 index 1c8700bc05616..0000000000000 Binary files a/docs/images/TimeFilter.jpg and /dev/null differ diff --git a/docs/images/VizEditor.jpg b/docs/images/VizEditor.jpg deleted file mode 100644 index 8aabfe544a0cd..0000000000000 Binary files a/docs/images/VizEditor.jpg and /dev/null differ diff --git a/docs/images/add-column-button.png b/docs/images/add-column-button.png deleted file mode 100644 index 6f44d0facf41f..0000000000000 Binary files a/docs/images/add-column-button.png and /dev/null differ diff --git a/docs/images/add_filter_field.png b/docs/images/add_filter_field.png deleted file mode 100644 index 2052559cf5273..0000000000000 Binary files a/docs/images/add_filter_field.png and /dev/null differ diff --git a/docs/images/add_filter_operator.png b/docs/images/add_filter_operator.png deleted file mode 100644 index fd7d42a9d1b98..0000000000000 Binary files a/docs/images/add_filter_operator.png and /dev/null differ diff --git a/docs/images/add_filter_value.png b/docs/images/add_filter_value.png deleted file mode 100644 index d357c6e5a3013..0000000000000 Binary files a/docs/images/add_filter_value.png and /dev/null differ diff --git a/docs/images/auto_format_after.png b/docs/images/auto_format_after.png deleted file mode 100644 index 018e82951b64f..0000000000000 Binary files a/docs/images/auto_format_after.png and /dev/null differ diff --git a/docs/images/auto_format_before.png b/docs/images/auto_format_before.png deleted file mode 100644 index 2535aa1af5240..0000000000000 Binary files a/docs/images/auto_format_before.png and /dev/null differ diff --git a/docs/images/auto_format_bulk.png b/docs/images/auto_format_bulk.png deleted file mode 100644 index 92cb688473ab7..0000000000000 Binary files a/docs/images/auto_format_bulk.png and /dev/null differ diff --git a/docs/images/autorefresh-intervals.png b/docs/images/autorefresh-intervals.png deleted file mode 100644 index 49be46fefd4aa..0000000000000 Binary files a/docs/images/autorefresh-intervals.png and /dev/null differ diff --git a/docs/images/autorefresh-pause.png b/docs/images/autorefresh-pause.png deleted file mode 100644 index 5a83c4587c961..0000000000000 Binary files a/docs/images/autorefresh-pause.png and /dev/null differ diff --git a/docs/images/autorefresh.png b/docs/images/autorefresh.png deleted file mode 100644 index 9a6225b9007bd..0000000000000 Binary files a/docs/images/autorefresh.png and /dev/null differ diff --git a/docs/images/bar-terms-agg.png b/docs/images/bar-terms-agg.png deleted file mode 100644 index b0b62b9e53213..0000000000000 Binary files a/docs/images/bar-terms-agg.png and /dev/null differ diff --git a/docs/images/bar-terms-subagg.png b/docs/images/bar-terms-subagg.png deleted file mode 100644 index 37cf5486eff1e..0000000000000 Binary files a/docs/images/bar-terms-subagg.png and /dev/null differ diff --git a/docs/images/canvas-align-elements.gif b/docs/images/canvas-align-elements.gif deleted file mode 100644 index 0081308d68795..0000000000000 Binary files a/docs/images/canvas-align-elements.gif and /dev/null differ diff --git a/docs/images/canvas-background-color-picker.gif b/docs/images/canvas-background-color-picker.gif deleted file mode 100644 index bd22941b35f5d..0000000000000 Binary files a/docs/images/canvas-background-color-picker.gif and /dev/null differ diff --git a/docs/images/canvas-click-drag-element.gif b/docs/images/canvas-click-drag-element.gif deleted file mode 100644 index 34f4268caf6f5..0000000000000 Binary files a/docs/images/canvas-click-drag-element.gif and /dev/null differ diff --git a/docs/images/canvas-distribute-elements.gif b/docs/images/canvas-distribute-elements.gif deleted file mode 100644 index 685d76ba22e40..0000000000000 Binary files a/docs/images/canvas-distribute-elements.gif and /dev/null differ diff --git a/docs/images/canvas-download-json.gif b/docs/images/canvas-download-json.gif deleted file mode 100644 index c0c0025e508c1..0000000000000 Binary files a/docs/images/canvas-download-json.gif and /dev/null differ diff --git a/docs/images/canvas-ecommerce.png b/docs/images/canvas-ecommerce.png deleted file mode 100644 index 58c0612881341..0000000000000 Binary files a/docs/images/canvas-ecommerce.png and /dev/null differ diff --git a/docs/images/canvas-element-order.gif b/docs/images/canvas-element-order.gif deleted file mode 100644 index e2911367e7dfa..0000000000000 Binary files a/docs/images/canvas-element-order.gif and /dev/null differ diff --git a/docs/images/canvas-embed_workpad.gif b/docs/images/canvas-embed_workpad.gif deleted file mode 100644 index 97a79d775fe36..0000000000000 Binary files a/docs/images/canvas-embed_workpad.gif and /dev/null differ diff --git a/docs/images/canvas-fullscreen.gif b/docs/images/canvas-fullscreen.gif deleted file mode 100644 index 2eebd3b511000..0000000000000 Binary files a/docs/images/canvas-fullscreen.gif and /dev/null differ diff --git a/docs/images/canvas-move-pixel.gif b/docs/images/canvas-move-pixel.gif deleted file mode 100644 index 228f0f7b7e18c..0000000000000 Binary files a/docs/images/canvas-move-pixel.gif and /dev/null differ diff --git a/docs/images/canvas-resize-element.gif b/docs/images/canvas-resize-element.gif deleted file mode 100644 index d2d2ab06bbb42..0000000000000 Binary files a/docs/images/canvas-resize-element.gif and /dev/null differ diff --git a/docs/images/canvas-zoom.gif b/docs/images/canvas-zoom.gif deleted file mode 100644 index 584118d75a43f..0000000000000 Binary files a/docs/images/canvas-zoom.gif and /dev/null differ diff --git a/docs/images/canvas_create_image.png b/docs/images/canvas_create_image.png deleted file mode 100644 index 7b7c38102e4c9..0000000000000 Binary files a/docs/images/canvas_create_image.png and /dev/null differ diff --git a/docs/images/canvas_map-time-filter.gif b/docs/images/canvas_map-time-filter.gif deleted file mode 100644 index 301d7f4b44158..0000000000000 Binary files a/docs/images/canvas_map-time-filter.gif and /dev/null differ diff --git a/docs/images/canvas_share_autoplay_480.gif b/docs/images/canvas_share_autoplay_480.gif deleted file mode 100644 index 84a108e58d3dc..0000000000000 Binary files a/docs/images/canvas_share_autoplay_480.gif and /dev/null differ diff --git a/docs/images/canvas_share_hidetoolbar_480.gif b/docs/images/canvas_share_hidetoolbar_480.gif deleted file mode 100644 index 282783057776a..0000000000000 Binary files a/docs/images/canvas_share_hidetoolbar_480.gif and /dev/null differ diff --git a/docs/images/canvas_workpad_3_page.png b/docs/images/canvas_workpad_3_page.png deleted file mode 100644 index 9a60ed3d00f60..0000000000000 Binary files a/docs/images/canvas_workpad_3_page.png and /dev/null differ diff --git a/docs/images/canvas_workpad_edit_style.png b/docs/images/canvas_workpad_edit_style.png deleted file mode 100644 index d12ae2cd81b8f..0000000000000 Binary files a/docs/images/canvas_workpad_edit_style.png and /dev/null differ diff --git a/docs/images/canvas_workpad_weblog.png b/docs/images/canvas_workpad_weblog.png deleted file mode 100755 index 7b6ebee5c9554..0000000000000 Binary files a/docs/images/canvas_workpad_weblog.png and /dev/null differ diff --git a/docs/images/controls/controls_options.png b/docs/images/controls/controls_options.png deleted file mode 100644 index aab93d5cd4be0..0000000000000 Binary files a/docs/images/controls/controls_options.png and /dev/null differ diff --git a/docs/images/controls/dropdown_control_editor.png b/docs/images/controls/dropdown_control_editor.png deleted file mode 100644 index 36a360dcd275e..0000000000000 Binary files a/docs/images/controls/dropdown_control_editor.png and /dev/null differ diff --git a/docs/images/controls/range_slider_editor.png b/docs/images/controls/range_slider_editor.png deleted file mode 100644 index 8d6c5a68d1d24..0000000000000 Binary files a/docs/images/controls/range_slider_editor.png and /dev/null differ diff --git a/docs/images/discover-compass.png b/docs/images/discover-compass.png deleted file mode 100644 index 0e3c80ff75a74..0000000000000 Binary files a/docs/images/discover-compass.png and /dev/null differ diff --git a/docs/images/edit_filter_query.png b/docs/images/edit_filter_query.png deleted file mode 100644 index 367a2a8578b8b..0000000000000 Binary files a/docs/images/edit_filter_query.png and /dev/null differ diff --git a/docs/images/filter-actions.png b/docs/images/filter-actions.png deleted file mode 100644 index 92feef2f0dbbb..0000000000000 Binary files a/docs/images/filter-actions.png and /dev/null differ diff --git a/docs/images/filter-allbuttons.png b/docs/images/filter-allbuttons.png deleted file mode 100644 index 3d6951812daa7..0000000000000 Binary files a/docs/images/filter-allbuttons.png and /dev/null differ diff --git a/docs/images/filter-sample.png b/docs/images/filter-sample.png deleted file mode 100644 index 9d2540720a5a2..0000000000000 Binary files a/docs/images/filter-sample.png and /dev/null differ diff --git a/docs/images/goal.png b/docs/images/goal.png deleted file mode 100644 index 04f16e8cd3e74..0000000000000 Binary files a/docs/images/goal.png and /dev/null differ diff --git a/docs/images/history.png b/docs/images/history.png deleted file mode 100644 index 8e6674e1f2c69..0000000000000 Binary files a/docs/images/history.png and /dev/null differ diff --git a/docs/images/labelbutton.png b/docs/images/labelbutton.png deleted file mode 100644 index 287a588802384..0000000000000 Binary files a/docs/images/labelbutton.png and /dev/null differ diff --git a/docs/images/lens_remove_layer.png b/docs/images/lens_remove_layer.png deleted file mode 100644 index 4184e5b846870..0000000000000 Binary files a/docs/images/lens_remove_layer.png and /dev/null differ diff --git a/docs/images/management-index-management.png b/docs/images/management-index-management.png deleted file mode 100644 index 1b1ff9226147c..0000000000000 Binary files a/docs/images/management-index-management.png and /dev/null differ diff --git a/docs/images/management-upgrade-assistant-8.0.png b/docs/images/management-upgrade-assistant-8.0.png deleted file mode 100644 index 4b37262414039..0000000000000 Binary files a/docs/images/management-upgrade-assistant-8.0.png and /dev/null differ diff --git a/docs/images/management-watcher-buttons.png b/docs/images/management-watcher-buttons.png deleted file mode 100644 index ce114ccf1bac9..0000000000000 Binary files a/docs/images/management-watcher-buttons.png and /dev/null differ diff --git a/docs/images/management_rolled_dashboard.png b/docs/images/management_rolled_dashboard.png deleted file mode 100755 index db731420fb96a..0000000000000 Binary files a/docs/images/management_rolled_dashboard.png and /dev/null differ diff --git a/docs/images/management_rollups_visualization.png b/docs/images/management_rollups_visualization.png deleted file mode 100755 index bba3b6e91a953..0000000000000 Binary files a/docs/images/management_rollups_visualization.png and /dev/null differ diff --git a/docs/images/markdown-example.png b/docs/images/markdown-example.png deleted file mode 100644 index 79daa1298883d..0000000000000 Binary files a/docs/images/markdown-example.png and /dev/null differ diff --git a/docs/images/multiple_requests.png b/docs/images/multiple_requests.png deleted file mode 100644 index e4fd010d54b4b..0000000000000 Binary files a/docs/images/multiple_requests.png and /dev/null differ diff --git a/docs/images/regionmap.png b/docs/images/regionmap.png deleted file mode 100644 index 97f2594e8bee6..0000000000000 Binary files a/docs/images/regionmap.png and /dev/null differ diff --git a/docs/images/search-button.jpg b/docs/images/search-button.jpg deleted file mode 100644 index b7787cac4bf6a..0000000000000 Binary files a/docs/images/search-button.jpg and /dev/null differ diff --git a/docs/images/security_base_all.png b/docs/images/security_base_all.png deleted file mode 100644 index 2aef42132ef21..0000000000000 Binary files a/docs/images/security_base_all.png and /dev/null differ diff --git a/docs/images/share-short-link.png b/docs/images/share-short-link.png deleted file mode 100644 index bf7f7782c4e2a..0000000000000 Binary files a/docs/images/share-short-link.png and /dev/null differ diff --git a/docs/images/time-filter-absolute.jpg b/docs/images/time-filter-absolute.jpg deleted file mode 100644 index bc54d57f0f737..0000000000000 Binary files a/docs/images/time-filter-absolute.jpg and /dev/null differ diff --git a/docs/images/time-filter-relative.jpg b/docs/images/time-filter-relative.jpg deleted file mode 100644 index 77beca3a3fd46..0000000000000 Binary files a/docs/images/time-filter-relative.jpg and /dev/null differ diff --git a/docs/images/time-filter.jpg b/docs/images/time-filter.jpg deleted file mode 100644 index e437f314d849d..0000000000000 Binary files a/docs/images/time-filter.jpg and /dev/null differ diff --git a/docs/images/time-picker-step.jpg b/docs/images/time-picker-step.jpg deleted file mode 100644 index 90c749776bb5d..0000000000000 Binary files a/docs/images/time-picker-step.jpg and /dev/null differ diff --git a/docs/images/time-picker.jpg b/docs/images/time-picker.jpg deleted file mode 100644 index 25830082d5919..0000000000000 Binary files a/docs/images/time-picker.jpg and /dev/null differ diff --git a/docs/images/timelion-arg-help.jpg b/docs/images/timelion-arg-help.jpg deleted file mode 100644 index 3e471c861d46b..0000000000000 Binary files a/docs/images/timelion-arg-help.jpg and /dev/null differ diff --git a/docs/images/timelion-read-only-badge.png b/docs/images/timelion-read-only-badge.png deleted file mode 100644 index 19ffbfed6335a..0000000000000 Binary files a/docs/images/timelion-read-only-badge.png and /dev/null differ diff --git a/docs/images/timelion-save01.png b/docs/images/timelion-save01.png deleted file mode 100644 index 47a33c2d36d43..0000000000000 Binary files a/docs/images/timelion-save01.png and /dev/null differ diff --git a/docs/images/timelion-save02.png b/docs/images/timelion-save02.png deleted file mode 100644 index 348b084ee5259..0000000000000 Binary files a/docs/images/timelion-save02.png and /dev/null differ diff --git a/docs/images/tsvb-annotations.png b/docs/images/tsvb-annotations.png deleted file mode 100644 index 22238db7e9e91..0000000000000 Binary files a/docs/images/tsvb-annotations.png and /dev/null differ diff --git a/docs/images/tsvb-data-tab-derivative-example.png b/docs/images/tsvb-data-tab-derivative-example.png deleted file mode 100644 index 66368baf1e16a..0000000000000 Binary files a/docs/images/tsvb-data-tab-derivative-example.png and /dev/null differ diff --git a/docs/images/tsvb-data-tab-label.png b/docs/images/tsvb-data-tab-label.png deleted file mode 100644 index 43d1fc64f4446..0000000000000 Binary files a/docs/images/tsvb-data-tab-label.png and /dev/null differ diff --git a/docs/images/tsvb-data-tab-series-options-time-series.png b/docs/images/tsvb-data-tab-series-options-time-series.png deleted file mode 100644 index 4c7ddadd38d95..0000000000000 Binary files a/docs/images/tsvb-data-tab-series-options-time-series.png and /dev/null differ diff --git a/docs/images/tsvb-data-tab-series-options.png b/docs/images/tsvb-data-tab-series-options.png deleted file mode 100644 index afadc3349bfe4..0000000000000 Binary files a/docs/images/tsvb-data-tab-series-options.png and /dev/null differ diff --git a/docs/images/tutorial-full-inspect2.png b/docs/images/tutorial-full-inspect2.png deleted file mode 100644 index 23c840f545ec3..0000000000000 Binary files a/docs/images/tutorial-full-inspect2.png and /dev/null differ diff --git a/docs/images/tutorial-sample-discover-2.png b/docs/images/tutorial-sample-discover-2.png deleted file mode 100644 index 4f4b2dc920ccb..0000000000000 Binary files a/docs/images/tutorial-sample-discover-2.png and /dev/null differ diff --git a/docs/images/tutorial-sample-inspect2.png b/docs/images/tutorial-sample-inspect2.png deleted file mode 100644 index b487d21e5cc02..0000000000000 Binary files a/docs/images/tutorial-sample-inspect2.png and /dev/null differ diff --git a/docs/images/tutorial-visualize-pie-1.png b/docs/images/tutorial-visualize-pie-1.png deleted file mode 100644 index 109829c01f28c..0000000000000 Binary files a/docs/images/tutorial-visualize-pie-1.png and /dev/null differ diff --git a/docs/images/visualize-flow.png b/docs/images/visualize-flow.png deleted file mode 100644 index bc00ff52a8d6e..0000000000000 Binary files a/docs/images/visualize-flow.png and /dev/null differ diff --git a/docs/images/visualize-icon.png b/docs/images/visualize-icon.png deleted file mode 100644 index af7ad18e9bf79..0000000000000 Binary files a/docs/images/visualize-icon.png and /dev/null differ diff --git a/docs/images/visualize_coordinate_map_example.png b/docs/images/visualize_coordinate_map_example.png deleted file mode 100644 index 24f03376adade..0000000000000 Binary files a/docs/images/visualize_coordinate_map_example.png and /dev/null differ diff --git a/docs/images/visualize_region_map_example.png b/docs/images/visualize_region_map_example.png deleted file mode 100644 index cf89e92625ece..0000000000000 Binary files a/docs/images/visualize_region_map_example.png and /dev/null differ diff --git a/docs/images/viz-fit-bounds.png b/docs/images/viz-fit-bounds.png deleted file mode 100644 index 9c0ddb89d7ddd..0000000000000 Binary files a/docs/images/viz-fit-bounds.png and /dev/null differ diff --git a/docs/images/viz-lat-long-filter.png b/docs/images/viz-lat-long-filter.png deleted file mode 100644 index 30c139b224565..0000000000000 Binary files a/docs/images/viz-lat-long-filter.png and /dev/null differ diff --git a/docs/images/viz-zoom.png b/docs/images/viz-zoom.png deleted file mode 100644 index 661e053130882..0000000000000 Binary files a/docs/images/viz-zoom.png and /dev/null differ diff --git a/docs/images/follower_indices.png b/docs/management/alerting/images/follower_indices.png similarity index 100% rename from docs/images/follower_indices.png rename to docs/management/alerting/images/follower_indices.png diff --git a/docs/images/actions_icon.png b/docs/management/images/actions_icon.png similarity index 100% rename from docs/images/actions_icon.png rename to docs/management/images/actions_icon.png diff --git a/docs/images/add_remote_cluster.png b/docs/management/images/add_remote_cluster.png similarity index 100% rename from docs/images/add_remote_cluster.png rename to docs/management/images/add_remote_cluster.png diff --git a/docs/images/auto_follow_pattern.png b/docs/management/images/auto_follow_pattern.png similarity index 100% rename from docs/images/auto_follow_pattern.png rename to docs/management/images/auto_follow_pattern.png diff --git a/docs/images/colorformatter.png b/docs/management/images/colorformatter.png similarity index 100% rename from docs/images/colorformatter.png rename to docs/management/images/colorformatter.png diff --git a/docs/images/cross-cluster-replication-list-view.png b/docs/management/images/cross-cluster-replication-list-view.png similarity index 100% rename from docs/images/cross-cluster-replication-list-view.png rename to docs/management/images/cross-cluster-replication-list-view.png diff --git a/docs/images/index-lifecycle-policies-create.png b/docs/management/images/index-lifecycle-policies-create.png similarity index 100% rename from docs/images/index-lifecycle-policies-create.png rename to docs/management/images/index-lifecycle-policies-create.png diff --git a/docs/images/index_lifecycle_policies_options.png b/docs/management/images/index_lifecycle_policies_options.png similarity index 100% rename from docs/images/index_lifecycle_policies_options.png rename to docs/management/images/index_lifecycle_policies_options.png diff --git a/docs/images/index_management_add_policy.png b/docs/management/images/index_management_add_policy.png similarity index 100% rename from docs/images/index_management_add_policy.png rename to docs/management/images/index_management_add_policy.png diff --git a/docs/images/management-create-rollup-bar-chart.png b/docs/management/images/management-create-rollup-bar-chart.png similarity index 100% rename from docs/images/management-create-rollup-bar-chart.png rename to docs/management/images/management-create-rollup-bar-chart.png diff --git a/docs/images/management-index-patterns.png b/docs/management/images/management-index-patterns.png similarity index 100% rename from docs/images/management-index-patterns.png rename to docs/management/images/management-index-patterns.png diff --git a/docs/images/management-index-read-only-badge.png b/docs/management/images/management-index-read-only-badge.png similarity index 100% rename from docs/images/management-index-read-only-badge.png rename to docs/management/images/management-index-read-only-badge.png diff --git a/docs/images/management-index-templates-mappings.png b/docs/management/images/management-index-templates-mappings.png similarity index 100% rename from docs/images/management-index-templates-mappings.png rename to docs/management/images/management-index-templates-mappings.png diff --git a/docs/images/management-index-templates.png b/docs/management/images/management-index-templates.png similarity index 100% rename from docs/images/management-index-templates.png rename to docs/management/images/management-index-templates.png diff --git a/docs/images/management-license.png b/docs/management/images/management-license.png similarity index 100% rename from docs/images/management-license.png rename to docs/management/images/management-license.png diff --git a/docs/images/management-rollup-index-pattern.png b/docs/management/images/management-rollup-index-pattern.png similarity index 100% rename from docs/images/management-rollup-index-pattern.png rename to docs/management/images/management-rollup-index-pattern.png diff --git a/docs/images/management-saved-objects.png b/docs/management/images/management-saved-objects.png similarity index 100% rename from docs/images/management-saved-objects.png rename to docs/management/images/management-saved-objects.png diff --git a/docs/images/management-upgrade-assistant-9.0.png b/docs/management/images/management-upgrade-assistant-9.0.png similarity index 100% rename from docs/images/management-upgrade-assistant-9.0.png rename to docs/management/images/management-upgrade-assistant-9.0.png diff --git a/docs/images/management_create_rollup_job.png b/docs/management/images/management_create_rollup_job.png similarity index 100% rename from docs/images/management_create_rollup_job.png rename to docs/management/images/management_create_rollup_job.png diff --git a/docs/images/management_create_rollup_menu.png b/docs/management/images/management_create_rollup_menu.png similarity index 100% rename from docs/images/management_create_rollup_menu.png rename to docs/management/images/management_create_rollup_menu.png diff --git a/docs/images/management_index_create_wizard.png b/docs/management/images/management_index_create_wizard.png similarity index 100% rename from docs/images/management_index_create_wizard.png rename to docs/management/images/management_index_create_wizard.png diff --git a/docs/images/management_index_details.png b/docs/management/images/management_index_details.png similarity index 100% rename from docs/images/management_index_details.png rename to docs/management/images/management_index_details.png diff --git a/docs/images/management_index_labels.png b/docs/management/images/management_index_labels.png similarity index 100% rename from docs/images/management_index_labels.png rename to docs/management/images/management_index_labels.png diff --git a/docs/images/management_rollup_job_dashboard.png b/docs/management/images/management_rollup_job_dashboard.png similarity index 100% rename from docs/images/management_rollup_job_dashboard.png rename to docs/management/images/management_rollup_job_dashboard.png diff --git a/docs/images/management_rollup_job_details.png b/docs/management/images/management_rollup_job_details.png similarity index 100% rename from docs/images/management_rollup_job_details.png rename to docs/management/images/management_rollup_job_details.png diff --git a/docs/images/management_rollup_job_vis.png b/docs/management/images/management_rollup_job_vis.png similarity index 100% rename from docs/images/management_rollup_job_vis.png rename to docs/management/images/management_rollup_job_vis.png diff --git a/docs/images/management_rollup_list.png b/docs/management/images/management_rollup_list.png similarity index 100% rename from docs/images/management_rollup_list.png rename to docs/management/images/management_rollup_list.png diff --git a/docs/images/remote-clusters-list-view.png b/docs/management/images/remote-clusters-list-view.png similarity index 100% rename from docs/images/remote-clusters-list-view.png rename to docs/management/images/remote-clusters-list-view.png diff --git a/docs/images/settings-read-only-badge.png b/docs/management/images/settings-read-only-badge.png similarity index 100% rename from docs/images/settings-read-only-badge.png rename to docs/management/images/settings-read-only-badge.png diff --git a/docs/images/tutorial-ilm-custom-policy.png b/docs/management/images/tutorial-ilm-custom-policy.png similarity index 100% rename from docs/images/tutorial-ilm-custom-policy.png rename to docs/management/images/tutorial-ilm-custom-policy.png diff --git a/docs/images/tutorial-ilm-delete-phase-creation.png b/docs/management/images/tutorial-ilm-delete-phase-creation.png similarity index 100% rename from docs/images/tutorial-ilm-delete-phase-creation.png rename to docs/management/images/tutorial-ilm-delete-phase-creation.png diff --git a/docs/images/tutorial-ilm-delete-rollover.png b/docs/management/images/tutorial-ilm-delete-rollover.png similarity index 100% rename from docs/images/tutorial-ilm-delete-rollover.png rename to docs/management/images/tutorial-ilm-delete-rollover.png diff --git a/docs/images/tutorial-ilm-hotphaserollover-default.png b/docs/management/images/tutorial-ilm-hotphaserollover-default.png similarity index 100% rename from docs/images/tutorial-ilm-hotphaserollover-default.png rename to docs/management/images/tutorial-ilm-hotphaserollover-default.png diff --git a/docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png b/docs/management/images/tutorial-ilm-modify-default-warm-phase-rollover.png similarity index 100% rename from docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png rename to docs/management/images/tutorial-ilm-modify-default-warm-phase-rollover.png diff --git a/docs/images/add-data-fv.png b/docs/setup/images/add-data-fv.png similarity index 100% rename from docs/images/add-data-fv.png rename to docs/setup/images/add-data-fv.png diff --git a/docs/images/add-data-tutorials.png b/docs/setup/images/add-data-tutorials.png similarity index 100% rename from docs/images/add-data-tutorials.png rename to docs/setup/images/add-data-tutorials.png diff --git a/docs/images/data-viz-homepage.jpg b/docs/setup/images/data-viz-homepage.jpg similarity index 100% rename from docs/images/data-viz-homepage.jpg rename to docs/setup/images/data-viz-homepage.jpg diff --git a/docs/images/kibana-status-page-7_5_0.png b/docs/setup/images/kibana-status-page-7_5_0.png similarity index 100% rename from docs/images/kibana-status-page-7_5_0.png rename to docs/setup/images/kibana-status-page-7_5_0.png diff --git a/docs/uptime-guide/alerting.asciidoc b/docs/uptime-guide/alerting.asciidoc deleted file mode 100644 index bf9e7693fc7a5..0000000000000 --- a/docs/uptime-guide/alerting.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[role="xpack"] -[[uptime-alerting]] - -=== Uptime alerting - -The Uptime app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] -feature. It provides a set of built-in actions and Uptime specific threshold alerts for you to use -and enables central management of all alerts from {kibana-ref}/management.html[Kibana Management]. - -[role="screenshot"] -image::images/create-alert.png[Create alert] - -[float] -==== Monitor status alerts - -To receive alerts when a monitor goes down, use the alerting menu at the top of the -overview page. Use a query in the alert flyout to determine which monitors to check -with your alert. If you already have a query in the overview page search bar it will -be carried over into this box. - -[role="screenshot"] -image::images/monitor-status-alert.png[Create monitor status alert flyout] - -[float] -==== TLS alerts - -Uptime also provides the ability to create an alert that will notify you when one or -more of your monitors have a TLS certificate that will expire within some threshold, -or when its age exceeds a limit. The values for these thresholds are configurable on -the <>. - -[role="screenshot"] -image::images/tls-alert.png[Create TLS alert flyout] diff --git a/docs/uptime-guide/app-overview.asciidoc b/docs/uptime-guide/app-overview.asciidoc deleted file mode 100644 index 692489a7ad311..0000000000000 --- a/docs/uptime-guide/app-overview.asciidoc +++ /dev/null @@ -1,70 +0,0 @@ -[role="xpack"] -[[uptime-app]] -== Uptime app - -The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. -You can explore endpoint status over time, drill down into specific monitors, -and view a high-level snapshot of your environment at any point in time. - -[role="screenshot"] -image::images/uptime-overview.png[Uptime app overview] - -[role="xpack"] -[[uptime-app-overview]] -=== Overview - -The Uptime overview helps you quickly identify and diagnose outages and -other connectivity issues within your network or environment. You can use the date range -selection that is global to the Uptime app, to highlight -an absolute date range, or a relative one, similar to other areas of {kib}. - -[float] -=== Filter bar - -The Filter bar enables you to quickly view specific groups of monitors, or even -an individual monitor if you have defined many. - -This control allows you to use automated filter options, as well as input custom filter -text to select specific monitors by field, URL, ID, and other attributes. - -[role="screenshot"] -image::images/filter-bar.png[Filter bar] - -[float] -=== Snapshot panel - -The Snapshot panel displays the overall -status of the environment you're monitoring or a subset of those monitors. -You can see the total number of detected monitors within the selected -Uptime date range, along with the number of monitors -in an `up` or `down` state, which is based on the last check reported by Heartbeat -for each monitor. - -Next to the counts, there is a histogram displaying the change over time throughout the -selected date range. - -[role="screenshot"] -image::images/snapshot-view.png[Snapshot view] - -[float] -=== Monitor list - -Information about individual monitors is displayed in the monitor list and provides a quick -way to navigate to a more in-depth visualization for interesting hosts or endpoints. - -The information displayed includes the recent status of a host or endpoint, when the monitor was last checked, its -ID and URL, and its IP address. There is also sparkline showing its check status over time. - -[role="screenshot"] -image::images/monitor-list.png[Monitor list] - -[float] -=== Observability integrations - -The Monitor list also contains a menu of available integrations. When Uptime detects Kubernetes or -Docker related host information, it provides links to open the Metrics app or Logs app pre-filtered -for this host. Additionally, to help you quickly determine if these solutions contain data relevant to you, -this feature contains links to filter the other views on the host's IP address. - -[role="screenshot"] -image::images/observability_integrations.png[Observability integrations] diff --git a/docs/uptime-guide/certificates.asciidoc b/docs/uptime-guide/certificates.asciidoc deleted file mode 100644 index 58db91aa080eb..0000000000000 --- a/docs/uptime-guide/certificates.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[role="xpack"] -[[uptime-certificates]] - -=== Certificates - -The certificates page enables you to visualize TLS certificate data in your indices. In addition to the -common name, associated monitors, issuer information, and SHA fingerprints, Uptime also assigns a status -derived from the threshold values in the <>. - -Several of the columns on this page are sortable. You can use the search bar at the top of the view -to find values in most of the TLS-related fields in your Uptime indices. Additionally, using the `Alerts` -dropdown at the top of the page you can create a TLS alert. - -[role="screenshot"] -image::images/certificates-page.png[Certificates] diff --git a/docs/uptime-guide/deployment-arch.asciidoc b/docs/uptime-guide/deployment-arch.asciidoc deleted file mode 100644 index c1b2f596c6665..0000000000000 --- a/docs/uptime-guide/deployment-arch.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[uptime-deployment-arch]] -== Deployment Architecture - -There are multiple ways to deploy Uptime and Heartbeat. -Use the information in this section to determine the best deployment for you. -A guiding principle is that when an outage takes down the service being monitored it should not also take down Heartbeat. -You want Heartbeat to be functioning even when your service is not, so the guidelines here help you maximize this possibility. - -Heartbeat is commonly run as a centralized service within a data center. -While it is possible to run it as a separate "sidecar" process paired with each process/container, we recommend against it. -Running Heartbeat centrally ensures you will still be able to see monitoring data in the event of an overloaded, disconnected, or otherwise malfunctioning server. - -For further redundancy, you may want to deploy multiple Heartbeats across geographic and network boundaries to provide more data. -To do so, specify Heartbeat's observer {heartbeat-ref}/configuration-observer-options.html[geo options]. - -Some examples might be: - -* **A site served from a content delivery network (CDN) with points of presence (POPs) around the globe:** -To check if your site is reachable via CDN POPS, you may want to have multiple Heartbeat instances at different data centers around the world. -* **A service within a single data center that is accessed across multiple VPNs:** -Set up one Heartbeat instance within the VPN the service operates from, and another within an additional VPN that users access the service from. -Having both instances helps pinpoint network errors in the event of an outage. -* **A single service running primarily in a US east coast data center, with a hot failover located in a US west coast data center:** -In each data center, run a Heartbeat instance that checks both the local copy of the service and its counterpart across the country. -Set up two monitors in each region, one for the local service and one for the remote service. -In the event of a data center failure it will be immediately apparent if the service had a connectivity issue to the outside world or if the failure was only internal. diff --git a/docs/uptime-guide/images/cert-exp.png b/docs/uptime-guide/images/cert-exp.png deleted file mode 100644 index cd87668db96dd..0000000000000 Binary files a/docs/uptime-guide/images/cert-exp.png and /dev/null differ diff --git a/docs/uptime-guide/images/certificates-page.png b/docs/uptime-guide/images/certificates-page.png deleted file mode 100644 index 598aae982cd6a..0000000000000 Binary files a/docs/uptime-guide/images/certificates-page.png and /dev/null differ diff --git a/docs/uptime-guide/images/check-history.png b/docs/uptime-guide/images/check-history.png deleted file mode 100644 index aac5efd9b91d3..0000000000000 Binary files a/docs/uptime-guide/images/check-history.png and /dev/null differ diff --git a/docs/uptime-guide/images/create-alert.png b/docs/uptime-guide/images/create-alert.png deleted file mode 100644 index 54a0c400cad4c..0000000000000 Binary files a/docs/uptime-guide/images/create-alert.png and /dev/null differ diff --git a/docs/uptime-guide/images/crosshair-example.png b/docs/uptime-guide/images/crosshair-example.png deleted file mode 100644 index f9e89c4f622e0..0000000000000 Binary files a/docs/uptime-guide/images/crosshair-example.png and /dev/null differ diff --git a/docs/uptime-guide/images/filter-bar.png b/docs/uptime-guide/images/filter-bar.png deleted file mode 100644 index b7c424d3d0d91..0000000000000 Binary files a/docs/uptime-guide/images/filter-bar.png and /dev/null differ diff --git a/docs/uptime-guide/images/indices.png b/docs/uptime-guide/images/indices.png deleted file mode 100644 index 4090747b6726c..0000000000000 Binary files a/docs/uptime-guide/images/indices.png and /dev/null differ diff --git a/docs/uptime-guide/images/monitor-charts.png b/docs/uptime-guide/images/monitor-charts.png deleted file mode 100644 index 522f34662657e..0000000000000 Binary files a/docs/uptime-guide/images/monitor-charts.png and /dev/null differ diff --git a/docs/uptime-guide/images/monitor-list.png b/docs/uptime-guide/images/monitor-list.png deleted file mode 100644 index c9a8eccf01f6e..0000000000000 Binary files a/docs/uptime-guide/images/monitor-list.png and /dev/null differ diff --git a/docs/uptime-guide/images/monitor-status-alert.png b/docs/uptime-guide/images/monitor-status-alert.png deleted file mode 100644 index 847a0f58f02ce..0000000000000 Binary files a/docs/uptime-guide/images/monitor-status-alert.png and /dev/null differ diff --git a/docs/uptime-guide/images/observability_integrations.png b/docs/uptime-guide/images/observability_integrations.png deleted file mode 100644 index 3b23aa2dbd2a5..0000000000000 Binary files a/docs/uptime-guide/images/observability_integrations.png and /dev/null differ diff --git a/docs/uptime-guide/images/settings.png b/docs/uptime-guide/images/settings.png deleted file mode 100644 index d19b7f842ea68..0000000000000 Binary files a/docs/uptime-guide/images/settings.png and /dev/null differ diff --git a/docs/uptime-guide/images/snapshot-view.png b/docs/uptime-guide/images/snapshot-view.png deleted file mode 100644 index b6f07fb0721aa..0000000000000 Binary files a/docs/uptime-guide/images/snapshot-view.png and /dev/null differ diff --git a/docs/uptime-guide/images/status-bar.png b/docs/uptime-guide/images/status-bar.png deleted file mode 100644 index fd72e2b78c2a0..0000000000000 Binary files a/docs/uptime-guide/images/status-bar.png and /dev/null differ diff --git a/docs/uptime-guide/images/tls-alert.png b/docs/uptime-guide/images/tls-alert.png deleted file mode 100644 index 19efe07838903..0000000000000 Binary files a/docs/uptime-guide/images/tls-alert.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-multi-deployment.png b/docs/uptime-guide/images/uptime-multi-deployment.png deleted file mode 100644 index 5440d91e48e23..0000000000000 Binary files a/docs/uptime-guide/images/uptime-multi-deployment.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-overview.png b/docs/uptime-guide/images/uptime-overview.png deleted file mode 100644 index 25c88b2d14287..0000000000000 Binary files a/docs/uptime-guide/images/uptime-overview.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-setup.png b/docs/uptime-guide/images/uptime-setup.png deleted file mode 100644 index 398125202fc4a..0000000000000 Binary files a/docs/uptime-guide/images/uptime-setup.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-simple-deployment.png b/docs/uptime-guide/images/uptime-simple-deployment.png deleted file mode 100644 index f46dfdb2b8b86..0000000000000 Binary files a/docs/uptime-guide/images/uptime-simple-deployment.png and /dev/null differ diff --git a/docs/uptime-guide/index.asciidoc b/docs/uptime-guide/index.asciidoc deleted file mode 100644 index 01a93cb454ea9..0000000000000 --- a/docs/uptime-guide/index.asciidoc +++ /dev/null @@ -1,22 +0,0 @@ - -include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] -include::{asciidoc-dir}/../../shared/attributes.asciidoc[] - -= Uptime monitoring guide - -include::overview.asciidoc[] - -include::install.asciidoc[] - -include::deployment-arch.asciidoc[] - -include::app-overview.asciidoc[] - -include::monitor.asciidoc[] - -include::settings.asciidoc[] - -include::certificates.asciidoc[] - -include::alerting.asciidoc[] - diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc deleted file mode 100644 index 05b9c6665562f..0000000000000 --- a/docs/uptime-guide/install.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -[[install-uptime]] -== Install Uptime - -The easiest way to get started with Elastic Uptime is by using our hosted {es} Service on Elastic Cloud. -The {es} Service is available on both AWS and GCP, -and automatically configures {es} and {kib}. - -[float] -=== Hosted Elasticsearch Service - -Skip managing your own {es} and {kib} instance by using our -https://www.elastic.co/cloud/elasticsearch-service[hosted {es} Service] on -Elastic Cloud. - -{ess-trial}[Try out the {es} Service for free], -then jump straight to <>. - -[float] -[[before-installation]] -=== Install the stack yourself - -If you'd rather install the stack yourself, -first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility. - -* <> -* <> -* <> - -[[install-elasticsearch]] -=== Step 1: Install Elasticsearch - -Install an {es} cluster, start it up, and make sure it's running. - -. Verify that your system meets the -https://www.elastic.co/support/matrix#matrix_jvm[minimum JVM requirements] for {es}. -. {stack-gs}/get-started-elastic-stack.html#install-elasticsearch[Install Elasticsearch]. -. {stack-gs}/get-started-elastic-stack.html#_make_sure_elasticsearch_is_up_and_running[Make sure elasticsearch is up and running]. - -[[install-kibana]] -=== Step 2: Install Kibana - -Install {kib}, start it up, and open up the web interface: - -. {stack-gs}/get-started-elastic-stack.html#install-kibana[Install Kibana]. -. {stack-gs}/get-started-elastic-stack.html#_launch_the_kibana_web_interface[Launch the Kibana Web Interface]. - -[[install-heartbeat]] -=== Step 3: Install and configure Heartbeat - -Uptime requires the setup of monitors in Heartbeat. -These monitors provide the data you'll be visualizing in the {kibana-ref}/xpack-uptime.html[Uptime app]. - -For instructions on installing and configuring Heartbeat, see the *Setup Instructions* in {kib}. -Additional information is available in {heartbeat-ref}/heartbeat-configuration.html[Configure Heartbeat]. - -[role="screenshot"] -image::images/uptime-setup.png[Installation instructions on the Uptime page in Kibana] - -[[setup-security]] -=== Step 4: Set up Security - -Secure your installation by following the {heartbeat-ref}/securing-heartbeat.html[Secure Heartbeat] documentation. - -[float] -==== Important considerations - -* Make sure you're using the same major versions of Heartbeat and {kib}. - -* Index patterns tell {kib} which {es} indices you want to explore. -The Uptime app requires a +heartbeat-{major-version-only}*+ index pattern. -If you have configured a different index pattern, you can use {ref}/indices-aliases.html[index aliases] to ensure data is recognized by the Uptime app. - -After you install and configure Heartbeat, -the {kibana-ref}/xpack-uptime.html[Uptime app] is automatically populated with the Heartbeat monitors. diff --git a/docs/uptime-guide/monitor.asciidoc b/docs/uptime-guide/monitor.asciidoc deleted file mode 100644 index bb5d315cf63eb..0000000000000 --- a/docs/uptime-guide/monitor.asciidoc +++ /dev/null @@ -1,59 +0,0 @@ -[role="xpack"] -[[uptime-monitor]] -=== Monitor - -The Monitor page helps you gain insights into the performance -of a specific network endpoint. A detailed visualization of -the monitor's request duration over time, as well as the `up`/`down` -status over time, is displayed. By configuring Machine Learning jobs -on this page, you can also also detect anomalies in response time data. - - -==== Status panel - -The Status panel displays a quick summary of the latest information -regarding your monitor. You can view its latest status, click a link to -visit the targeted URL, see its most recent request duration, and determine the -amount of time that has elapsed since the last check. - -When two Heartbeat instances are configured in different geographic locations -the map will show each location as a pinpoint on the map, along with the -amount of time elapsed since data was last received from that location. - -[role="screenshot"] -image::images/status-bar.png[Status bar] - - -[float] -==== Monitor charts - -The Monitor charts visualize information over the time specified in the -date range. These charts help you gain insights into how quickly requests are being resolved -by the targeted endpoint, and give you a sense of how frequently a host or endpoint -was down in your selected timespan. - -[role="screenshot"] -image::images/monitor-charts.png[Monitor charts] - -The Monitor duration chart displays request duration information for your monitor. -The area surrounding the line is the range of request time for the corresponding -bucket. The line is the average time. In the upper right hand of this panel -you can enable Anomaly detection using Machine Learning. When response times change -in an unexpected way the time range in which they occurred are highlighted with a color. - -The pings over time chart is a graphical representation of the check statuses over time. -Hover over the charts to display crosshairs with specific numeric data. - -[role="screenshot"] -image::images/crosshair-example.png[Chart crosshair] - -[float] -==== Check history - -The Check history table lists the total count of this monitor's checks for the selected -date range. To help find recent problems on a per-check basis, you can filter the checks -by status and location. This table can help you gain some insight into more granular details -about recent individual data points that Heartbeat is logging about your host or endpoint. - -[role="screenshot"] -image::images/check-history.png[Check history view] diff --git a/docs/uptime-guide/overview.asciidoc b/docs/uptime-guide/overview.asciidoc deleted file mode 100644 index ab230b27f8cda..0000000000000 --- a/docs/uptime-guide/overview.asciidoc +++ /dev/null @@ -1,57 +0,0 @@ -[role="xpack"] -[[uptime-overview]] -== Elastic Uptime overview - -++++ -Overview -++++ - -Elastic Uptime enables you to monitor the availability and response times of applications and services in real time and to detect problems before they affect users. - -Elastic Uptime helps you to understand uptime and response time characteristics for your services and applications. -It can be deployed both inside and outside your organization's network, so that you can analyze problems from multiple vantage points. - -Elastic Uptime uses these components: *Heartbeat*, *Elasticsearch* and *Kibana*. - -[float] -=== Heartbeat - -{heartbeat-ref}/index.html[Heartbeat] is an open source data shipper that performs uptime monitoring. -Elastic Uptime uses Heartbeat to collect monitoring data from your target applications and services, and ship it to Elasticsearch. - -[float] -=== Elasticsearch - -{ref}/index.html[Elasticsearch] is a highly scalable, open source, search and analytics engine. -Elasticsearch can store, search, and analyze large volumes of data in near real-time. -Elastic Uptime uses Elasticsearch to store monitoring data from Heartbeat in Elasticsearch documents. - -[float] -=== Kibana - -{kibana-ref}/index.html[Kibana] is an open source analytics and visualization platform designed to work with Elasticsearch. -You can use Kibana to search, view, and interact with data stored in Elasticsearch. -You can easily perform advanced data analysis and visualize your data in a variety of charts, tables, and maps. - -The {kibana-ref}/xpack-uptime.html[Elasticsearch Uptime app] in Kibana provides a dedicated user interface for viewing uptime data and identifying problem areas. - -[float] -=== Example deployments -// ++ I like the Infra/logging diagram which shows Metrics and Logging apps as separate components inside Kibana -// ++ In diagram, should be Uptime app, not Uptime UI, possibly even Elastic Uptime? Also applies to Metrics/Logging/APM. -// ++ Need more whitespace around components. - -In this simple deployment, a single instance of Heartbeat is deployed at a single monitoring location to monitor a single service. -The Heartbeat instance sends the monitoring data to Elasticsearch. -Then you can use the Uptime app in Kibana to view the data from Heartbeat and determine the status of the service. - -image::images/uptime-simple-deployment.png[Uptime simple deployment] - -In this deployment, two instances of Heartbeat are deployed at two different monitoring locations. -Both instances monitor the same service. -The Heartbeat instances send the monitoring data to Elasticsearch. -As before, you can use the Uptime app in Kibana to view the Heartbeat data and determine the status of the service. -When a failure occurs, the multiple monitoring locations enable you to pinpoint the area in which the failure has occurred. - -image::images/uptime-multi-deployment.png[Uptime multiple server deployment] - diff --git a/docs/uptime-guide/settings.asciidoc b/docs/uptime-guide/settings.asciidoc deleted file mode 100644 index 59f9af631bfa7..0000000000000 --- a/docs/uptime-guide/settings.asciidoc +++ /dev/null @@ -1,51 +0,0 @@ -[role="xpack"] -[[uptime-settings]] - -=== Settings - -The Uptime settings page lets you change which Heartbeat indices are displayed -by the uptime app. Users must have the 'all' permission to modify items on this page. -Uptime settings apply to the current space only. Use different settings in different -spaces to segment different uptime use cases and domains. - -==== Indices - -Imagine your organization has one team for internal IT services, and another -for public services. Each team operates independently and is only responsible for its -own services. In this scenario, you might set up separate Heartbeat instances for each team, -writing out to index patterns named `it-heartbeat-\*`, and `external-heartbeat-\*`. You would -create separate roles and users for each in Elasticsearch, each with access to their own spaces, -named `it` and `external` respectively. Within each space you would navigate to the settings page -and set the correct index pattern to match only the indices that space is allowed to access. - -Note: The pattern set here only restricts what the Uptime app shows. Users may still be able -to manually query Elasticsearch for data outside this pattern. - -[role="screenshot"] -image::images/indices.png[Heartbeat indices] - -See the {kibana-ref}/uptime-security.html[Uptime security] and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security] -docs for more information. - -==== Certificate thresholds - -You can modify settings in this section to control how Uptime will visualize your TLS values in -the <>. These settings also determine which certificates will be -selected by any TLS alert you define. - -There are two fields, `age` and `expiration`. Use the `age` threshold to specify when Uptime should warn -you about certificates that have been valid for too long. Use the `expiration` threshold to specify when Uptime should warn you -about certificates that have approaching expiration dates. - -For example, a common security requirement is to make sure that none of your organization's TLS certificates have been -valid for longer than one year. Modifying the `Age limit` field's value to 365 days will help you keep track of which -certificates you may want to refresh. - -Likewise, to see which of your TLS certificates are close to expiring ahead of time, specify -an `Expiration threshold` on this page. When the count of a certificate's remaining valid days falls -below this threshold, Uptime will consider it in a warning state. When you define a TLS alert, you receive a -notification from Uptime about the certificate. - -[role="screenshot"] -image::images/cert-exp.png[Certification expiration thresholds] - diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 0468ab042e57e..5fd85a1045265 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -68,11 +68,11 @@ Then, select the *Integrations* tab and click the *New Integration* button. * If you are creating a new service for your integration, go to https://support.pagerduty.com/docs/services-and-integrations#section-configuring-services-and-integrations[Configuring Services and Integrations] -and follow the steps outlined in the *Create a New Service* section, selecting *Elastic* as the *Integration Type* in step 4. +and follow the steps outlined in the *Create a New Service* section, selecting *Elastic Alerts* as the *Integration Type* in step 4. Continue with the <> section once you have finished these steps. . Enter an *Integration Name* in the format Elastic-service-name (for example, Elastic-Alerting or Kibana-APM-Alerting) -and select Elastic from the *Integration Type* menu. +and select *Elastic Alerts* from the *Integration Type* menu. . Click *Add Integration* to save your new integration. + You will be redirected to the *Integrations* tab for your service. An Integration Key is generated on this screen. diff --git a/docs/images/Dashboard_add_new_visualization.png b/docs/user/dashboard/images/Dashboard_add_new_visualization.png similarity index 100% rename from docs/images/Dashboard_add_new_visualization.png rename to docs/user/dashboard/images/Dashboard_add_new_visualization.png diff --git a/docs/images/Dashboard_add_visualization.png b/docs/user/dashboard/images/Dashboard_add_visualization.png similarity index 100% rename from docs/images/Dashboard_add_visualization.png rename to docs/user/dashboard/images/Dashboard_add_visualization.png diff --git a/docs/images/Dashboard_example.png b/docs/user/dashboard/images/Dashboard_example.png similarity index 100% rename from docs/images/Dashboard_example.png rename to docs/user/dashboard/images/Dashboard_example.png diff --git a/docs/images/Dashboard_inspect.png b/docs/user/dashboard/images/Dashboard_inspect.png similarity index 100% rename from docs/images/Dashboard_inspect.png rename to docs/user/dashboard/images/Dashboard_inspect.png diff --git a/docs/images/clone_panel.gif b/docs/user/dashboard/images/clone_panel.gif similarity index 100% rename from docs/images/clone_panel.gif rename to docs/user/dashboard/images/clone_panel.gif diff --git a/docs/images/dashboard-read-only-badge.png b/docs/user/dashboard/images/dashboard-read-only-badge.png similarity index 100% rename from docs/images/dashboard-read-only-badge.png rename to docs/user/dashboard/images/dashboard-read-only-badge.png diff --git a/docs/images/time_range_per_panel.gif b/docs/user/dashboard/images/time_range_per_panel.gif similarity index 100% rename from docs/images/time_range_per_panel.gif rename to docs/user/dashboard/images/time_range_per_panel.gif diff --git a/docs/images/intro-dashboard.png b/docs/user/introduction/images/intro-dashboard.png similarity index 100% rename from docs/images/intro-dashboard.png rename to docs/user/introduction/images/intro-dashboard.png diff --git a/docs/images/intro-data-tutorial.png b/docs/user/introduction/images/intro-data-tutorial.png similarity index 100% rename from docs/images/intro-data-tutorial.png rename to docs/user/introduction/images/intro-data-tutorial.png diff --git a/docs/images/intro-discover.png b/docs/user/introduction/images/intro-discover.png similarity index 100% rename from docs/images/intro-discover.png rename to docs/user/introduction/images/intro-discover.png diff --git a/docs/images/intro-kibana.png b/docs/user/introduction/images/intro-kibana.png similarity index 100% rename from docs/images/intro-kibana.png rename to docs/user/introduction/images/intro-kibana.png diff --git a/docs/images/intro-management.png b/docs/user/introduction/images/intro-management.png similarity index 100% rename from docs/images/intro-management.png rename to docs/user/introduction/images/intro-management.png diff --git a/docs/images/intro-spaces.jpg b/docs/user/introduction/images/intro-spaces.jpg similarity index 100% rename from docs/images/intro-spaces.jpg rename to docs/user/introduction/images/intro-spaces.jpg diff --git a/docs/images/monitoring-dashboard.png b/docs/user/monitoring/images/monitoring-dashboard.png similarity index 100% rename from docs/images/monitoring-dashboard.png rename to docs/user/monitoring/images/monitoring-dashboard.png diff --git a/docs/images/report-automate-csv.png b/docs/user/reporting/images/report-automate-csv.png similarity index 100% rename from docs/images/report-automate-csv.png rename to docs/user/reporting/images/report-automate-csv.png diff --git a/docs/images/report-automate-pdf.png b/docs/user/reporting/images/report-automate-pdf.png similarity index 100% rename from docs/images/report-automate-pdf.png rename to docs/user/reporting/images/report-automate-pdf.png diff --git a/docs/images/add-bucket.png b/docs/visualize/images/add-bucket.png similarity index 100% rename from docs/images/add-bucket.png rename to docs/visualize/images/add-bucket.png diff --git a/docs/images/apply-changes-button.png b/docs/visualize/images/apply-changes-button.png similarity index 100% rename from docs/images/apply-changes-button.png rename to docs/visualize/images/apply-changes-button.png diff --git a/docs/images/color-picker.png b/docs/visualize/images/color-picker.png similarity index 100% rename from docs/images/color-picker.png rename to docs/visualize/images/color-picker.png diff --git a/docs/images/dashboard-controls.png b/docs/visualize/images/dashboard-controls.png similarity index 100% rename from docs/images/dashboard-controls.png rename to docs/visualize/images/dashboard-controls.png diff --git a/docs/images/gauge.png b/docs/visualize/images/gauge.png similarity index 100% rename from docs/images/gauge.png rename to docs/visualize/images/gauge.png diff --git a/docs/images/lens_data_info.png b/docs/visualize/images/lens_data_info.png similarity index 100% rename from docs/images/lens_data_info.png rename to docs/visualize/images/lens_data_info.png diff --git a/docs/images/lens_drag_drop.gif b/docs/visualize/images/lens_drag_drop.gif similarity index 100% rename from docs/images/lens_drag_drop.gif rename to docs/visualize/images/lens_drag_drop.gif diff --git a/docs/images/lens_suggestions.gif b/docs/visualize/images/lens_suggestions.gif similarity index 100% rename from docs/images/lens_suggestions.gif rename to docs/visualize/images/lens_suggestions.gif diff --git a/docs/images/lens_tutorial_1.png b/docs/visualize/images/lens_tutorial_1.png similarity index 100% rename from docs/images/lens_tutorial_1.png rename to docs/visualize/images/lens_tutorial_1.png diff --git a/docs/images/lens_tutorial_2.png b/docs/visualize/images/lens_tutorial_2.png similarity index 100% rename from docs/images/lens_tutorial_2.png rename to docs/visualize/images/lens_tutorial_2.png diff --git a/docs/images/lens_tutorial_3.png b/docs/visualize/images/lens_tutorial_3.png similarity index 100% rename from docs/images/lens_tutorial_3.png rename to docs/visualize/images/lens_tutorial_3.png diff --git a/docs/images/lens_viz_types.png b/docs/visualize/images/lens_viz_types.png similarity index 100% rename from docs/images/lens_viz_types.png rename to docs/visualize/images/lens_viz_types.png diff --git a/docs/images/markdown_example_1.png b/docs/visualize/images/markdown_example_1.png similarity index 100% rename from docs/images/markdown_example_1.png rename to docs/visualize/images/markdown_example_1.png diff --git a/docs/images/markdown_example_2.png b/docs/visualize/images/markdown_example_2.png similarity index 100% rename from docs/images/markdown_example_2.png rename to docs/visualize/images/markdown_example_2.png diff --git a/docs/images/markdown_example_3.png b/docs/visualize/images/markdown_example_3.png similarity index 100% rename from docs/images/markdown_example_3.png rename to docs/visualize/images/markdown_example_3.png diff --git a/docs/images/markdown_example_4.png b/docs/visualize/images/markdown_example_4.png similarity index 100% rename from docs/images/markdown_example_4.png rename to docs/visualize/images/markdown_example_4.png diff --git a/docs/images/timelion-conditional01.png b/docs/visualize/images/timelion-conditional01.png similarity index 100% rename from docs/images/timelion-conditional01.png rename to docs/visualize/images/timelion-conditional01.png diff --git a/docs/images/timelion-conditional02.png b/docs/visualize/images/timelion-conditional02.png similarity index 100% rename from docs/images/timelion-conditional02.png rename to docs/visualize/images/timelion-conditional02.png diff --git a/docs/images/timelion-conditional03.png b/docs/visualize/images/timelion-conditional03.png similarity index 100% rename from docs/images/timelion-conditional03.png rename to docs/visualize/images/timelion-conditional03.png diff --git a/docs/images/timelion-conditional04.png b/docs/visualize/images/timelion-conditional04.png similarity index 100% rename from docs/images/timelion-conditional04.png rename to docs/visualize/images/timelion-conditional04.png diff --git a/docs/images/timelion-create01.png b/docs/visualize/images/timelion-create01.png similarity index 100% rename from docs/images/timelion-create01.png rename to docs/visualize/images/timelion-create01.png diff --git a/docs/images/timelion-create02.png b/docs/visualize/images/timelion-create02.png similarity index 100% rename from docs/images/timelion-create02.png rename to docs/visualize/images/timelion-create02.png diff --git a/docs/images/timelion-create03.png b/docs/visualize/images/timelion-create03.png similarity index 100% rename from docs/images/timelion-create03.png rename to docs/visualize/images/timelion-create03.png diff --git a/docs/images/timelion-customize01.png b/docs/visualize/images/timelion-customize01.png similarity index 100% rename from docs/images/timelion-customize01.png rename to docs/visualize/images/timelion-customize01.png diff --git a/docs/images/timelion-customize02.png b/docs/visualize/images/timelion-customize02.png similarity index 100% rename from docs/images/timelion-customize02.png rename to docs/visualize/images/timelion-customize02.png diff --git a/docs/images/timelion-customize03.png b/docs/visualize/images/timelion-customize03.png similarity index 100% rename from docs/images/timelion-customize03.png rename to docs/visualize/images/timelion-customize03.png diff --git a/docs/images/timelion-customize04.png b/docs/visualize/images/timelion-customize04.png similarity index 100% rename from docs/images/timelion-customize04.png rename to docs/visualize/images/timelion-customize04.png diff --git a/docs/images/timelion-math01.png b/docs/visualize/images/timelion-math01.png similarity index 100% rename from docs/images/timelion-math01.png rename to docs/visualize/images/timelion-math01.png diff --git a/docs/images/timelion-math02.png b/docs/visualize/images/timelion-math02.png similarity index 100% rename from docs/images/timelion-math02.png rename to docs/visualize/images/timelion-math02.png diff --git a/docs/images/timelion-math03.png b/docs/visualize/images/timelion-math03.png similarity index 100% rename from docs/images/timelion-math03.png rename to docs/visualize/images/timelion-math03.png diff --git a/docs/images/timelion-math04.png b/docs/visualize/images/timelion-math04.png similarity index 100% rename from docs/images/timelion-math04.png rename to docs/visualize/images/timelion-math04.png diff --git a/docs/images/timelion-math05.png b/docs/visualize/images/timelion-math05.png similarity index 100% rename from docs/images/timelion-math05.png rename to docs/visualize/images/timelion-math05.png diff --git a/docs/images/tsvb-gauge.png b/docs/visualize/images/tsvb-gauge.png similarity index 100% rename from docs/images/tsvb-gauge.png rename to docs/visualize/images/tsvb-gauge.png diff --git a/docs/images/tsvb-markdown.png b/docs/visualize/images/tsvb-markdown.png similarity index 100% rename from docs/images/tsvb-markdown.png rename to docs/visualize/images/tsvb-markdown.png diff --git a/docs/images/tsvb-metric.png b/docs/visualize/images/tsvb-metric.png similarity index 100% rename from docs/images/tsvb-metric.png rename to docs/visualize/images/tsvb-metric.png diff --git a/docs/images/tsvb-screenshot.png b/docs/visualize/images/tsvb-screenshot.png similarity index 100% rename from docs/images/tsvb-screenshot.png rename to docs/visualize/images/tsvb-screenshot.png diff --git a/docs/images/tsvb-table.png b/docs/visualize/images/tsvb-table.png similarity index 100% rename from docs/images/tsvb-table.png rename to docs/visualize/images/tsvb-table.png diff --git a/docs/images/tsvb-top-n.png b/docs/visualize/images/tsvb-top-n.png similarity index 100% rename from docs/images/tsvb-top-n.png rename to docs/visualize/images/tsvb-top-n.png diff --git a/docs/images/vega_lite_default.png b/docs/visualize/images/vega_lite_default.png similarity index 100% rename from docs/images/vega_lite_default.png rename to docs/visualize/images/vega_lite_default.png diff --git a/docs/images/visualize-date-histogram-split-1.png b/docs/visualize/images/visualize-date-histogram-split-1.png similarity index 100% rename from docs/images/visualize-date-histogram-split-1.png rename to docs/visualize/images/visualize-date-histogram-split-1.png diff --git a/docs/images/visualize-date-histogram-split-2.png b/docs/visualize/images/visualize-date-histogram-split-2.png similarity index 100% rename from docs/images/visualize-date-histogram-split-2.png rename to docs/visualize/images/visualize-date-histogram-split-2.png diff --git a/docs/images/visualize-date-histogram.png b/docs/visualize/images/visualize-date-histogram.png similarity index 100% rename from docs/images/visualize-date-histogram.png rename to docs/visualize/images/visualize-date-histogram.png diff --git a/docs/images/visualize-drag-reorder.png b/docs/visualize/images/visualize-drag-reorder.png similarity index 100% rename from docs/images/visualize-drag-reorder.png rename to docs/visualize/images/visualize-drag-reorder.png diff --git a/docs/images/visualize_heat_map_example.png b/docs/visualize/images/visualize_heat_map_example.png similarity index 100% rename from docs/images/visualize_heat_map_example.png rename to docs/visualize/images/visualize_heat_map_example.png diff --git a/examples/embeddable_examples/common/book_saved_object_attributes.ts b/examples/embeddable_examples/common/book_saved_object_attributes.ts new file mode 100644 index 0000000000000..62c08b7b81362 --- /dev/null +++ b/examples/embeddable_examples/common/book_saved_object_attributes.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectAttributes } from '../../../src/core/types'; + +export const BOOK_SAVED_OBJECT = 'book'; + +export interface BookSavedObjectAttributes extends SavedObjectAttributes { + title: string; + author?: string; + readIt?: boolean; +} diff --git a/examples/embeddable_examples/common/index.ts b/examples/embeddable_examples/common/index.ts index 726420fb9bdc3..55715113a12a2 100644 --- a/examples/embeddable_examples/common/index.ts +++ b/examples/embeddable_examples/common/index.ts @@ -18,3 +18,4 @@ */ export { TodoSavedObjectAttributes } from './todo_saved_object_attributes'; +export { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from './book_saved_object_attributes'; diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 486c6322fad93..8ae04c1f6c644 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable"], + "requiredPlugins": ["embeddable", "uiActions"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"] } diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx new file mode 100644 index 0000000000000..064e13c131a0a --- /dev/null +++ b/examples/embeddable_examples/public/book/book_component.tsx @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui'; + +import { EuiText } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; +import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable'; + +interface Props { + input: BookEmbeddableInput; + output: BookEmbeddableOutput; + embeddable: BookEmbeddable; +} + +function wrapSearchTerms(task?: string, search?: string) { + if (!search || !task) return task; + const parts = task.split(new RegExp(`(${search})`, 'g')); + return parts.map((part, i) => + part === search ? ( + + {part} + + ) : ( + part + ) + ); +} + +export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) { + const title = attributes?.title; + const author = attributes?.author; + const readIt = attributes?.readIt; + + return ( + + + + {title ? ( + + +

{wrapSearchTerms(title, search)},

+
+
+ ) : null} + {author ? ( + + +
-{wrapSearchTerms(author, search)}
+
+
+ ) : null} + {readIt ? ( + + + + ) : ( + + + + )} +
+
+
+ ); +} + +export const BookEmbeddableComponent = withEmbeddableSubscription< + BookEmbeddableInput, + BookEmbeddableOutput, + BookEmbeddable, + {} +>(BookEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx new file mode 100644 index 0000000000000..d49bd3280d97d --- /dev/null +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; +import { + Embeddable, + EmbeddableInput, + IContainer, + EmbeddableOutput, + SavedObjectEmbeddableInput, + AttributeService, +} from '../../../../src/plugins/embeddable/public'; +import { BookSavedObjectAttributes } from '../../common'; +import { BookEmbeddableComponent } from './book_component'; + +export const BOOK_EMBEDDABLE = 'book'; +export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; +export interface BookEmbeddableOutput extends EmbeddableOutput { + hasMatch: boolean; + attributes: BookSavedObjectAttributes; +} + +interface BookInheritedInput extends EmbeddableInput { + search?: string; +} + +export type BookByValueInput = { attributes: BookSavedObjectAttributes } & BookInheritedInput; +export type BookByReferenceInput = SavedObjectEmbeddableInput & BookInheritedInput; + +/** + * Returns whether any attributes contain the search string. If search is empty, true is returned. If + * there are no savedAttributes, false is returned. + * @param search - the search string + * @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId` + */ +function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttributes): boolean { + if (!search) return true; + if (!savedAttributes) return false; + return Boolean( + (savedAttributes.author && savedAttributes.author.match(search)) || + (savedAttributes.title && savedAttributes.title.match(search)) + ); +} + +export class BookEmbeddable extends Embeddable { + public readonly type = BOOK_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private savedObjectId?: string; + private attributes?: BookSavedObjectAttributes; + + constructor( + initialInput: BookEmbeddableInput, + private attributeService: AttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >, + { + parent, + }: { + parent?: IContainer; + } + ) { + super(initialInput, {} as BookEmbeddableOutput, parent); + + this.subscription = this.getInput$().subscribe(async () => { + const savedObjectId = (this.getInput() as BookByReferenceInput).savedObjectId; + const attributes = (this.getInput() as BookByValueInput).attributes; + if (this.attributes !== attributes || this.savedObjectId !== savedObjectId) { + this.savedObjectId = savedObjectId; + this.reload(); + } else { + this.updateOutput({ + attributes: this.attributes, + hasMatch: getHasMatch(this.input.search, this.attributes), + }); + } + }); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + } + + public async reload() { + this.attributes = await this.attributeService.unwrapAttributes(this.input); + + this.updateOutput({ + attributes: this.attributes, + hasMatch: getHasMatch(this.input.search, this.attributes), + }); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx new file mode 100644 index 0000000000000..f4a32fb498a2d --- /dev/null +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableStart, + IContainer, + AttributeService, + EmbeddableFactory, +} from '../../../../src/plugins/embeddable/public'; +import { + BookEmbeddable, + BOOK_EMBEDDABLE, + BookEmbeddableInput, + BookEmbeddableOutput, + BookByValueInput, + BookByReferenceInput, +} from './book_embeddable'; +import { CreateEditBookComponent } from './create_edit_book_component'; +import { OverlayStart } from '../../../../src/core/public'; + +interface StartServices { + getAttributeService: EmbeddableStart['getAttributeService']; + openModal: OverlayStart['openModal']; +} + +export type BookEmbeddableFactory = EmbeddableFactory< + BookEmbeddableInput, + BookEmbeddableOutput, + BookEmbeddable, + BookSavedObjectAttributes +>; + +export class BookEmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition< + BookEmbeddableInput, + BookEmbeddableOutput, + BookEmbeddable, + BookSavedObjectAttributes + > { + public readonly type = BOOK_EMBEDDABLE; + public savedObjectMetaData = { + name: 'Book', + includeFields: ['title', 'author', 'readIt'], + type: BOOK_SAVED_OBJECT, + getIconForSavedObject: () => 'pencil', + }; + + private attributeService?: AttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >; + + constructor(private getStartServices: () => Promise) {} + + public async isEditable() { + return true; + } + + public async create(input: BookEmbeddableInput, parent?: IContainer) { + return new BookEmbeddable(input, await this.getAttributeService(), { + parent, + }); + } + + public getDisplayName() { + return i18n.translate('embeddableExamples.book.displayName', { + defaultMessage: 'Book', + }); + } + + public async getExplicitInput(): Promise> { + const { openModal } = await this.getStartServices(); + return new Promise>((resolve) => { + const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => { + const wrappedAttributes = (await this.getAttributeService()).wrapAttributes( + attributes, + useRefType + ); + resolve(wrappedAttributes); + }; + const overlay = openModal( + toMountPoint( + { + onSave(attributes, useRefType); + overlay.close(); + }} + /> + ) + ); + }); + } + + private async getAttributeService() { + if (!this.attributeService) { + this.attributeService = await (await this.getStartServices()).getAttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >(this.type); + } + return this.attributeService; + } +} diff --git a/examples/embeddable_examples/public/book/create_edit_book_component.tsx b/examples/embeddable_examples/public/book/create_edit_book_component.tsx new file mode 100644 index 0000000000000..7e2d3cb9d88ab --- /dev/null +++ b/examples/embeddable_examples/public/book/create_edit_book_component.tsx @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState } from 'react'; +import { EuiModalBody, EuiCheckbox } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { EuiModalFooter } from '@elastic/eui'; +import { EuiModalHeader } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; +import { BookSavedObjectAttributes } from '../../common'; + +export function CreateEditBookComponent({ + savedObjectId, + attributes, + onSave, +}: { + savedObjectId?: string; + attributes?: BookSavedObjectAttributes; + onSave: (attributes: BookSavedObjectAttributes, useRefType: boolean) => void; +}) { + const [title, setTitle] = useState(attributes?.title ?? ''); + const [author, setAuthor] = useState(attributes?.author ?? ''); + const [readIt, setReadIt] = useState(attributes?.readIt ?? false); + return ( + + +

{`${savedObjectId ? 'Create new ' : 'Edit '}`}

+
+ + + setTitle(e.target.value)} + /> + + + setAuthor(e.target.value)} + /> + + + setReadIt(event.target.checked)} + /> + + + + onSave({ title, author, readIt }, false)} + > + {savedObjectId ? 'Unlink from library item' : 'Save and Return'} + + onSave({ title, author, readIt }, true)} + > + {savedObjectId ? 'Update library item' : 'Save to library'} + + +
+ ); +} diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx new file mode 100644 index 0000000000000..222f70e0be60f --- /dev/null +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { OverlayStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; +import { createAction } from '../../../../src/plugins/ui_actions/public'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { + ViewMode, + EmbeddableStart, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { + BookEmbeddable, + BOOK_EMBEDDABLE, + BookByReferenceInput, + BookByValueInput, +} from './book_embeddable'; +import { CreateEditBookComponent } from './create_edit_book_component'; + +interface StartServices { + openModal: OverlayStart['openModal']; + getAttributeService: EmbeddableStart['getAttributeService']; +} + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_EDIT_BOOK = 'ACTION_EDIT_BOOK'; + +export const createEditBookAction = (getStartServices: () => Promise) => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.edit', { defaultMessage: 'Edit Book' }), + type: ACTION_EDIT_BOOK, + order: 100, + getIconType: () => 'documents', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && embeddable.getInput().viewMode === ViewMode.EDIT + ); + }, + execute: async ({ embeddable }: ActionContext) => { + const { openModal, getAttributeService } = await getStartServices(); + const attributeService = getAttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >(BOOK_SAVED_OBJECT); + const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => { + const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable); + if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) { + // Remove the savedObejctId when un-linking + newInput.savedObjectId = null; + } + embeddable.updateInput(newInput); + if (useRefType) { + // Ensures that any duplicate embeddables also register the changes. This mirrors the behavior of going back and forth between apps + embeddable.getRoot().reload(); + } + }; + const overlay = openModal( + toMountPoint( + { + overlay.close(); + onSave(attributes, useRefType); + }} + /> + ) + ); + }, + }); diff --git a/examples/embeddable_examples/public/book/index.ts b/examples/embeddable_examples/public/book/index.ts new file mode 100644 index 0000000000000..46f44926e2152 --- /dev/null +++ b/examples/embeddable_examples/public/book/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './book_embeddable'; +export * from './book_embeddable_factory'; diff --git a/examples/embeddable_examples/public/create_sample_data.ts b/examples/embeddable_examples/public/create_sample_data.ts index bd5ade18aa91e..d598c32a182fe 100644 --- a/examples/embeddable_examples/public/create_sample_data.ts +++ b/examples/embeddable_examples/public/create_sample_data.ts @@ -18,9 +18,9 @@ */ import { SavedObjectsClientContract } from 'kibana/public'; -import { TodoSavedObjectAttributes } from '../common'; +import { TodoSavedObjectAttributes, BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../common'; -export async function createSampleData(client: SavedObjectsClientContract) { +export async function createSampleData(client: SavedObjectsClientContract, overwrite = true) { await client.create( 'todo', { @@ -30,7 +30,20 @@ export async function createSampleData(client: SavedObjectsClientContract) { }, { id: 'sample-todo-saved-object', - overwrite: true, + overwrite, + } + ); + + await client.create( + BOOK_SAVED_OBJECT, + { + title: 'Pillars of the Earth', + author: 'Ken Follett', + readIt: true, + }, + { + id: 'sample-book-saved-object', + overwrite, } ); } diff --git a/examples/embeddable_examples/public/index.ts b/examples/embeddable_examples/public/index.ts index ec007f7c626f0..86f50f2b6e114 100644 --- a/examples/embeddable_examples/public/index.ts +++ b/examples/embeddable_examples/public/index.ts @@ -26,6 +26,8 @@ export { export { ListContainer, LIST_CONTAINER, ListContainerFactory } from './list_container'; export { TODO_EMBEDDABLE, TodoEmbeddableFactory } from './todo'; +export { BOOK_EMBEDDABLE } from './book'; + import { EmbeddableExamplesPlugin } from './plugin'; export { diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index d65ca1e8e7e8d..95f4f5b41e198 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,14 +17,19 @@ * under the License. */ -import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; -import { CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; import { + EmbeddableSetup, + EmbeddableStart, + CONTEXT_MENU_TRIGGER, +} from '../../../src/plugins/embeddable/public'; +import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; +import { + HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddableFactoryDefinition, - HelloWorldEmbeddableFactory, } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoEmbeddableFactoryDefinition } from './todo'; + import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory, @@ -46,9 +51,17 @@ import { TodoRefEmbeddableFactory, TodoRefEmbeddableFactoryDefinition, } from './todo/todo_ref_embeddable_factory'; +import { ACTION_EDIT_BOOK, createEditBookAction } from './book/edit_book_action'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book/book_embeddable'; +import { + BookEmbeddableFactory, + BookEmbeddableFactoryDefinition, +} from './book/book_embeddable_factory'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; + uiActions: UiActionsStart; } export interface EmbeddableExamplesStartDependencies { @@ -62,6 +75,7 @@ interface ExampleEmbeddableFactories { getListContainerEmbeddableFactory: () => ListContainerFactory; getTodoEmbeddableFactory: () => TodoEmbeddableFactory; getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory; + getBookEmbeddableFactory: () => BookEmbeddableFactory; } export interface EmbeddableExamplesStart { @@ -69,6 +83,12 @@ export interface EmbeddableExamplesStart { factories: ExampleEmbeddableFactories; } +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable }; + } +} + export class EmbeddableExamplesPlugin implements Plugin< @@ -121,6 +141,20 @@ export class EmbeddableExamplesPlugin getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, })) ); + this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( + BOOK_EMBEDDABLE, + new BookEmbeddableFactoryDefinition(async () => ({ + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + openModal: (await core.getStartServices())[0].overlays.openModal, + })) + ); + + const editBookAction = createEditBookAction(async () => ({ + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + openModal: (await core.getStartServices())[0].overlays.openModal, + })); + deps.uiActions.registerAction(editBookAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id); } public start( diff --git a/examples/embeddable_examples/server/book_saved_object.ts b/examples/embeddable_examples/server/book_saved_object.ts new file mode 100644 index 0000000000000..f0aca57f7925f --- /dev/null +++ b/examples/embeddable_examples/server/book_saved_object.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsType } from 'kibana/server'; + +export const bookSavedObject: SavedObjectsType = { + name: 'book', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + title: { + type: 'keyword', + }, + author: { + type: 'keyword', + }, + readIt: { + type: 'boolean', + }, + }, + }, + migrations: {}, +}; diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts index d956b834d0d3c..1308ac9e0fc5e 100644 --- a/examples/embeddable_examples/server/plugin.ts +++ b/examples/embeddable_examples/server/plugin.ts @@ -19,10 +19,12 @@ import { Plugin, CoreSetup, CoreStart } from 'kibana/server'; import { todoSavedObject } from './todo_saved_object'; +import { bookSavedObject } from './book_saved_object'; export class EmbeddableExamplesPlugin implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(todoSavedObject); + core.savedObjects.registerType(bookSavedObject); } public start(core: CoreStart) {} diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index b2807f9a4c346..ca9675bb7f5a1 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -33,6 +33,7 @@ import { EmbeddableStart, IEmbeddable } from '../../../src/plugins/embeddable/pu import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, + BOOK_EMBEDDABLE, MULTI_TASK_TODO_EMBEDDABLE, SearchableListContainerFactory, } from '../../embeddable_examples/public'; @@ -72,6 +73,35 @@ export function EmbeddablePanelExample({ embeddableServices, searchListContainer tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'], }, }, + '4': { + type: BOOK_EMBEDDABLE, + explicitInput: { + id: '4', + savedObjectId: 'sample-book-saved-object', + }, + }, + '5': { + type: BOOK_EMBEDDABLE, + explicitInput: { + id: '5', + attributes: { + title: 'The Sympathizer', + author: 'Viet Thanh Nguyen', + readIt: true, + }, + }, + }, + '6': { + type: BOOK_EMBEDDABLE, + explicitInput: { + id: '6', + attributes: { + title: 'The Hobbit', + author: 'J.R.R. Tolkien', + readIt: false, + }, + }, + }, }, }; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index 90bea1c3aa293..f4200d6f47574 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -36,6 +36,8 @@ export function MochaReporterProvider({ getService }) { let originalLogWriters; let reporterCaptureStartTime; + const failuresOverTime = []; + return class MochaReporter extends Mocha.reporters.Base { constructor(runner, options) { super(runner, options); @@ -154,30 +156,50 @@ export function MochaReporterProvider({ getService }) { // - I started by trying to extract the Base.list() logic from mocha // but it's a lot more complicated than this is horrible. // - In order to fix the numbering and indentation we monkey-patch - // console.log and parse the logged output. + // Mocha.reporters.Base.consoleLog and parse the logged output. // let output = ''; - const realLog = console.log; - console.log = (...args) => (output += `${format(...args)}\n`); + const realLog = Mocha.reporters.Base.consoleLog; + Mocha.reporters.Base.consoleLog = (...args) => (output += `${format(...args)}\n`); try { Mocha.reporters.Base.list([runnable]); } finally { - console.log = realLog; + Mocha.reporters.Base.consoleLog = realLog; } + const outputLines = output.split('\n'); + + const errorMarkerStart = outputLines.reduce((index, line, i) => { + if (index >= 0) { + return index; + } + return /Error:/.test(line) ? i : index; + }, -1); + + const errorMessage = outputLines + // drop the first ${errorMarkerStart} lines, (empty + test title) + .slice(errorMarkerStart) + // move leading colors behind leading spaces + .map((line) => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) + .map((line) => ` ${line}`) + .join('\n'); + log.write( - `- ${colors.fail(`${symbols.err} fail: "${runnable.fullTitle()}"`)}` + - '\n' + - output - .split('\n') - // drop the first two lines, (empty + test title) - .slice(2) - // move leading colors behind leading spaces - .map((line) => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) - .map((line) => ` ${line}`) - .join('\n') + `- ${colors.fail(`${symbols.err} fail: ${runnable.fullTitle()}`)}` + '\n' + errorMessage ); + // Prefer to reuse the nice Mocha nested title format for final summary + const nestedTitleFormat = outputLines + .slice(1, errorMarkerStart) + .join('\n') + // make sure to remove the list number + .replace(/\d+\)/, ''); + + failuresOverTime.push({ + title: nestedTitleFormat, + error: errorMessage, + }); + // failed hooks trigger the `onFail(runnable)` callback, so we snapshot the logs for // them here. Tests will re-capture the snapshot in `onTestEnd()` snapshotLogsForRunnable(runnable); @@ -188,7 +210,7 @@ export function MochaReporterProvider({ getService }) { log.setWriters(originalLogWriters); } - writeEpilogue(log, this.stats); + writeEpilogue(log, this.stats, failuresOverTime); }; }; } diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js index 72a011ce510bc..0ee429067254b 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js @@ -20,7 +20,7 @@ import * as colors from './colors'; import { ms } from './ms'; -export function writeEpilogue(log, stats) { +export function writeEpilogue(log, stats, failuresDetail) { // header log.write(''); @@ -35,6 +35,12 @@ export function writeEpilogue(log, stats) { // failures if (stats.failures) { log.write('%d failing', stats.failures); + log.write(''); + failuresDetail.forEach(({ title, error }, i) => { + log.write('%d) %s', i + 1, title); + log.write(''); + log.write('%s', error); + }); } // footer diff --git a/src/core/public/application/scoped_history.mock.ts b/src/core/public/application/scoped_history.mock.ts index 56de97e630bf0..41c72306a99f9 100644 --- a/src/core/public/application/scoped_history.mock.ts +++ b/src/core/public/application/scoped_history.mock.ts @@ -27,7 +27,8 @@ const createMock = ({ hash = '', key, state, -}: Partial = {}) => { + ...overrides +}: Partial = {}) => { const mock: ScopedHistoryMock = { block: jest.fn(), createHref: jest.fn(), @@ -38,6 +39,7 @@ const createMock = ({ listen: jest.fn(), push: jest.fn(), replace: jest.fn(), + ...overrides, action: 'PUSH', length: 1, location: { diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index 7521e4a4bee86..7a7955ee745e8 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -45,7 +45,9 @@ * @public */ export interface SavedObjectsTypeMappingDefinition { - /** The dynamic property of the mapping. either `false` or 'strict'. Defaults to `false` */ + /** The dynamic property of the mapping, either `false` or `'strict'`. If + * unspecified `dynamic: 'strict'` will be inherited from the top-level + * index mappings. */ dynamic?: false | 'strict'; /** The underlying properties of the type mapping */ properties: SavedObjectsMappingProperties; @@ -134,7 +136,6 @@ export interface SavedObjectsCoreFieldMapping { null_value?: number | boolean | string; index?: boolean; doc_values?: boolean; - enabled?: boolean; fields?: { [subfield: string]: { type: string; @@ -146,14 +147,19 @@ export interface SavedObjectsCoreFieldMapping { /** * See {@link SavedObjectsFieldMapping} for documentation. * - * Note: this type intentially doesn't include a type definition for defining - * the `dynamic` mapping parameter. Saved Object fields should always inherit - * the `dynamic: 'strict'` paramater. If you are unsure of the shape of your - * data use `type: 'object', enabled: false` instead. - * * @public */ export interface SavedObjectsComplexFieldMapping { + /** + * The dynamic property of the mapping, either `false` or `'strict'`. If + * unspecified `dynamic: 'strict'` will be inherited from the top-level + * index mappings. + * + * Note: To limit the number of mapping fields Saved Object types should + * *never* use `dynamic: true`. + */ + dynamic?: false | 'strict'; + enabled?: boolean; doc_values?: boolean; type?: string; properties: SavedObjectsMappingProperties; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 86c79cbfb5824..f8b203bf66d6a 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -151,7 +151,7 @@ describe('IndexMigrator', () => { ); }); - test('retains mappings from the previous index', async () => { + test('retains unknown core field mappings from the previous index', async () => { const { callCluster } = testOpts; testOpts.mappingProperties = { foo: { type: 'text' } }; @@ -162,7 +162,7 @@ describe('IndexMigrator', () => { aliases: {}, mappings: { properties: { - author: { type: 'text' }, + unknown_core_field: { type: 'text' }, }, }, }, @@ -187,7 +187,66 @@ describe('IndexMigrator', () => { }, }, properties: { - author: { type: 'text' }, + unknown_core_field: { type: 'text' }, + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + + test('disables complex field mappings from unknown types in the previous index', async () => { + const { callCluster } = testOpts; + + testOpts.mappingProperties = { foo: { type: 'text' } }; + + withIndex(callCluster, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + unknown_complex_field: { properties: { description: { type: 'text' } } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(callCluster).toHaveBeenCalledWith('indices.create', { + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + unknown_complex_field: { dynamic: false, properties: {} }, foo: { type: 'text' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, diff --git a/src/core/server/saved_objects/migrations/core/migration_context.test.ts b/src/core/server/saved_objects/migrations/core/migration_context.test.ts new file mode 100644 index 0000000000000..34d8d94d5ddab --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/migration_context.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { disableUnknownTypeMappingFields } from './migration_context'; + +describe('disableUnknownTypeMappingFields', () => { + const sourceMappings = { + _meta: { + migrationMappingPropertyHashes: { + unknown_type: 'md5hash', + unknown_core_field: 'md5hash', + known_type: 'oldmd5hash', + }, + }, + properties: { + unknown_type: { + properties: { + unused_field: { type: 'text' }, + }, + }, + unknown_core_field: { type: 'keyword' }, + known_type: { + properties: { + field_1: { type: 'text' }, + old_field: { type: 'boolean' }, + }, + }, + }, + }; + const activeMappings = { + _meta: { + migrationMappingPropertyHashes: { + known_type: 'md5hash', + }, + }, + properties: { + known_type: { + properties: { + new_field: { type: 'binary' }, + field_1: { type: 'keyword' }, + }, + }, + }, + }; + const targetMappings = disableUnknownTypeMappingFields(activeMappings, sourceMappings); + + it('disables complex field mappings from unknown types in the source mappings', () => { + expect(targetMappings.properties.unknown_type).toEqual({ dynamic: false, properties: {} }); + }); + + it('retains unknown core field mappings from the source mappings', () => { + expect(targetMappings.properties.unknown_core_field).toEqual({ type: 'keyword' }); + }); + + it('overrides source mappings with known types from active mappings', () => { + expect(targetMappings.properties.known_type).toEqual({ + properties: { + new_field: { type: 'binary' }, + field_1: { type: 'keyword' }, // was type text in source mappings + // old_field was present in source but ommited in active mappings + }, + }); + }); + + it('retains the active mappings _meta ignoring any _meta fields in the source mappings', () => { + expect(targetMappings._meta).toEqual({ + migrationMappingPropertyHashes: { + known_type: 'md5hash', + }, + }); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index 3a6145f5d9623..adf1851a1aa75 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -26,7 +26,11 @@ import { Logger } from 'src/core/server/logging'; import { SavedObjectsSerializer } from '../../serialization'; -import { SavedObjectsTypeMappingDefinitions } from '../../mappings'; +import { + SavedObjectsTypeMappingDefinitions, + SavedObjectsMappingProperties, + IndexMapping, +} from '../../mappings'; import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; @@ -107,20 +111,68 @@ function createSourceContext(source: FullIndexInfo, alias: string) { function createDestContext( source: FullIndexInfo, alias: string, - mappingProperties: SavedObjectsTypeMappingDefinitions + typeMappingDefinitions: SavedObjectsTypeMappingDefinitions ): FullIndexInfo { - const activeMappings = buildActiveMappings(mappingProperties); + const targetMappings = disableUnknownTypeMappingFields( + buildActiveMappings(typeMappingDefinitions), + source.mappings + ); return { aliases: {}, exists: false, indexName: nextIndexName(source.indexName, alias), - mappings: { - ...activeMappings, - properties: { - ...source.mappings.properties, - ...activeMappings.properties, - }, + mappings: targetMappings, + }; +} + +/** + * Merges the active mappings and the source mappings while disabling the + * fields of any unknown Saved Object types present in the source index's + * mappings. + * + * Since the Saved Objects index has `dynamic: strict` defined at the + * top-level, only Saved Object types for which a mapping exists can be + * inserted into the index. To ensure that we can continue to store Saved + * Object documents belonging to a disabled plugin we define a mapping for all + * the unknown Saved Object types that were present in the source index's + * mappings. To limit the field count as much as possible, these unkwnown + * type's mappings are set to `dynamic: false`. + * + * (Since we're using the source index mappings instead of looking at actual + * document types in the inedx, we potentially add more "unknown types" than + * what would be necessary to support migrating all the data over to the + * target index.) + * + * @param activeMappings The mappings compiled from all the Saved Object types + * known to this Kibana node. + * @param sourceMappings The mappings of index used as the migration source. + * @returns The mappings that should be applied to the target index. + */ +export function disableUnknownTypeMappingFields( + activeMappings: IndexMapping, + sourceMappings: IndexMapping +): IndexMapping { + const targetTypes = Object.keys(activeMappings.properties); + + const disabledTypesProperties = Object.keys(sourceMappings.properties) + .filter((sourceType) => { + const isObjectType = 'properties' in sourceMappings.properties[sourceType]; + // Only Object/Nested datatypes can be excluded from the field count by + // using `dynamic: false`. + return !targetTypes.includes(sourceType) && isObjectType; + }) + .reduce((disabledTypesAcc, sourceType) => { + disabledTypesAcc[sourceType] = { dynamic: false, properties: {} }; + return disabledTypesAcc; + }, {} as SavedObjectsMappingProperties); + + return { + ...activeMappings, + properties: { + ...sourceMappings.properties, + ...disabledTypesProperties, + ...activeMappings.properties, }, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 107edf11bb6f4..efeafc9e68d35 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2022,6 +2022,9 @@ export interface SavedObjectsClientWrapperOptions { export interface SavedObjectsComplexFieldMapping { // (undocumented) doc_values?: boolean; + dynamic?: false | 'strict'; + // (undocumented) + enabled?: boolean; // (undocumented) properties: SavedObjectsMappingProperties; // (undocumented) @@ -2033,8 +2036,6 @@ export interface SavedObjectsCoreFieldMapping { // (undocumented) doc_values?: boolean; // (undocumented) - enabled?: boolean; - // (undocumented) fields?: { [subfield: string]: { type: string; diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh index 3832ec9b4076a..915759d4214f9 100755 --- a/src/dev/ci_setup/checkout_sibling_es.sh +++ b/src/dev/ci_setup/checkout_sibling_es.sh @@ -7,11 +7,10 @@ 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 [$WORKSPACE]!" + echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$PARENT_DIR]!" echo echo "Either define '${useExistingParamName}' or remove the existing '${project}' sibling." exit 1 @@ -22,9 +21,8 @@ function checkout_sibling { cloneBranch="" function clone_target_is_valid { - echo " -> checking for '${cloneBranch}' branch at ${cloneAuthor}/${project}" - if [[ -n "$(git ls-remote --heads "${repoAddress}${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then + if [[ -n "$(git ls-remote --heads "git@github.com:${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then return 0 else return 1 @@ -73,7 +71,7 @@ function checkout_sibling { fi echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..." - git clone -b "$cloneBranch" "${repoAddress}${cloneAuthor}/${project}.git" "$targetDir" --depth=1 + git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1 echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)" echo } @@ -89,12 +87,12 @@ function checkout_sibling { fi } -checkout_sibling "elasticsearch" "${WORKSPACE}/elasticsearch" "USE_EXISTING_ES" +checkout_sibling "elasticsearch" "${PARENT_DIR}/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="$WORKSPACE/elasticsearch" +ES_DIR="$PARENT_DIR/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 f96a2240917e2..343ff47199375 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -53,8 +53,6 @@ export PARENT_DIR="$parentDir" kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")" export KIBANA_PKG_BRANCH="$kbnBranch" -export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" - ### ### download node ### @@ -163,7 +161,7 @@ export -f checks-reporter-with-killswitch source "$KIBANA_DIR/src/dev/ci_setup/load_env_keys.sh" -ES_DIR="$WORKSPACE/elasticsearch" +ES_DIR="$PARENT_DIR/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/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 3bd3dc61c044f..7ff1d87f1bc55 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -21,7 +21,7 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t import path from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -// @ts-expect-error +// @ts-ignore import { normalizePath } from './utils'; const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); @@ -36,7 +36,6 @@ const defaultIntegrateOptions = { sourceFileName: localePath, dryRun: false, ignoreIncompatible: false, - ignoreMalformed: false, ignoreMissing: false, ignoreUnused: false, config: { diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index f9cd6dd1971c7..d8ccccca15559 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -31,8 +31,7 @@ import { normalizePath, readFileAsync, writeFileAsync, - verifyICUMessage, - // @ts-expect-error + // @ts-ignore } from './utils'; import { I18nConfig } from './config'; @@ -42,7 +41,6 @@ export interface IntegrateOptions { sourceFileName: string; targetFileName?: string; dryRun: boolean; - ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; @@ -107,23 +105,6 @@ export function verifyMessages( } } - for (const messageId of localizedMessagesIds) { - const defaultMessage = defaultMessagesMap.get(messageId); - if (defaultMessage) { - try { - const message = localizedMessagesMap.get(messageId)!; - verifyICUMessage(message); - } catch (err) { - if (options.ignoreMalformed) { - localizedMessagesMap.delete(messageId); - options.log.warning(`Malformed translation ignored (${messageId}): ${err}`); - } else { - errorMessage += `\nMalformed translation (${messageId}): ${err}\n`; - } - } - } - } - if (errorMessage) { throw createFailError(errorMessage); } diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts index afaf3cd875a8a..5900bf5aff252 100644 --- a/src/dev/i18n/tasks/check_compatibility.ts +++ b/src/dev/i18n/tasks/check_compatibility.ts @@ -22,14 +22,13 @@ import { integrateLocaleFiles, I18nConfig } from '..'; export interface I18nFlags { fix: boolean; - ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; } export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: ToolingLog) { - const { fix, ignoreIncompatible, ignoreUnused, ignoreMalformed, ignoreMissing } = flags; + const { fix, ignoreIncompatible, ignoreUnused, ignoreMissing } = flags; return config.translations.map((translationsPath) => ({ task: async ({ messages }: { messages: Map }) => { // If `fix` is set we should try apply all possible fixes and override translations file. @@ -38,7 +37,6 @@ export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: To ignoreIncompatible: fix || ignoreIncompatible, ignoreUnused: fix || ignoreUnused, ignoreMissing: fix || ignoreMissing, - ignoreMalformed: fix || ignoreMalformed, sourceFileName: translationsPath, targetFileName: fix ? translationsPath : undefined, config, diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 11a002fdbf4a8..1d1c3118e0852 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -208,28 +208,6 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI } } -/** - * Verifies valid ICU message. - * @param message ICU message. - * @param messageId ICU message id - * @returns {undefined} - */ -export function verifyICUMessage(message) { - try { - parser.parse(message); - } catch (error) { - if (error.name === 'SyntaxError') { - const errorWithContext = createParserErrorMessage(message, { - loc: { - line: error.location.start.line, - column: error.location.start.column - 1, - }, - message: error.message, - }); - throw errorWithContext; - } - } -} /** * Extracts value references from the ICU message. * @param message ICU message. diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index a2b05c6dc8a4e..fb74bed0f26f4 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -49,10 +49,8 @@ export async function generateNoticeFromSource({ productName, directory, log }: ignore: [ '{node_modules,build,target,dist,data,built_assets}/**', 'packages/*/{node_modules,build,target,dist}/**', - 'src/plugins/*/{node_modules,build,target,dist}/**', 'x-pack/{node_modules,build,target,dist,data}/**', 'x-pack/packages/*/{node_modules,build,target,dist}/**', - 'x-pack/plugins/*/{node_modules,build,target,dist}/**', ], }; diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 70eeedac2b8b6..97ea988b1de3a 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -36,7 +36,6 @@ run( async ({ flags: { 'ignore-incompatible': ignoreIncompatible, - 'ignore-malformed': ignoreMalformed, 'ignore-missing': ignoreMissing, 'ignore-unused': ignoreUnused, 'include-config': includeConfig, @@ -49,13 +48,12 @@ run( fix && (ignoreIncompatible !== undefined || ignoreUnused !== undefined || - ignoreMalformed !== undefined || ignoreMissing !== undefined) ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.` + )} none of the --ignore-incompatible, --ignore-unused or --ignore-missing is allowed when --fix is set.` ); } @@ -101,7 +99,6 @@ run( checkCompatibility( config, { - ignoreMalformed: !!ignoreMalformed, ignoreIncompatible: !!ignoreIncompatible, ignoreUnused: !!ignoreUnused, ignoreMissing: !!ignoreMissing, diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index 25c3ea32783aa..23d66fae9f26e 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -31,7 +31,6 @@ run( 'ignore-incompatible': ignoreIncompatible = false, 'ignore-missing': ignoreMissing = false, 'ignore-unused': ignoreUnused = false, - 'ignore-malformed': ignoreMalformed = false, 'include-config': includeConfig, path, source, @@ -67,13 +66,12 @@ run( typeof ignoreIncompatible !== 'boolean' || typeof ignoreUnused !== 'boolean' || typeof ignoreMissing !== 'boolean' || - typeof ignoreMalformed !== 'boolean' || typeof dryRun !== 'boolean' ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} --ignore-incompatible, --ignore-unused, --ignore-malformed, --ignore-missing, and --dry-run can't have values` + )} --ignore-incompatible, --ignore-unused, --ignore-missing, and --dry-run can't have values` ); } @@ -99,7 +97,6 @@ run( ignoreIncompatible, ignoreUnused, ignoreMissing, - ignoreMalformed, config, log, }); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 85bfd4a7a4d26..9d9f5616b5a33 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -26,4 +26,5 @@ export const storybookAliases = { infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js', + observability: 'x-pack/plugins/observability/scripts/storybook.js', }; diff --git a/src/plugins/charts/public/services/colors/mock.ts b/src/plugins/charts/public/services/colors/mock.ts index 924dbd6aa52a4..f88980e521dda 100644 --- a/src/plugins/charts/public/services/colors/mock.ts +++ b/src/plugins/charts/public/services/colors/mock.ts @@ -24,5 +24,5 @@ const colors = new ColorsService(); colors.init(coreMock.createSetup().uiSettings); export const colorsServiceMock: ColorsService = { - createColorLookupFunction: jest.fn(colors.createColorLookupFunction), + createColorLookupFunction: jest.fn(colors.createColorLookupFunction.bind(colors)), } as any; diff --git a/src/plugins/console/public/application/components/editor_example.tsx b/src/plugins/console/public/application/components/editor_example.tsx index e9e252e4ebb17..72a1056b1a866 100644 --- a/src/plugins/console/public/application/components/editor_example.tsx +++ b/src/plugins/console/public/application/components/editor_example.tsx @@ -18,8 +18,6 @@ */ import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import exampleText from 'raw-loader!../constants/help_example.txt'; import React, { useEffect } from 'react'; import { createReadOnlyAceEditor } from '../models/legacy_core_editor'; @@ -27,6 +25,17 @@ interface EditorExampleProps { panel: string; } +const exampleText = ` +# index a doc +PUT index/1 +{ + "body": "here" +} + +# and get it ... +GET index/1 +`; + export function EditorExample(props: EditorExampleProps) { const elemId = `help-example-${props.panel}`; const inputId = `help-example-${props.panel}-input`; diff --git a/src/plugins/console/public/application/constants/help_example.txt b/src/plugins/console/public/application/constants/help_example.txt deleted file mode 100644 index fd37c41367033..0000000000000 --- a/src/plugins/console/public/application/constants/help_example.txt +++ /dev/null @@ -1,8 +0,0 @@ -# index a doc -PUT index/1 -{ - "body": "here" -} - -# and get it ... -GET index/1 diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index a321bc7959c5c..8138e1c7f4dfd 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -60,6 +60,7 @@ import { ViewMode, ContainerOutput, EmbeddableInput, + SavedObjectEmbeddableInput, } from '../../../embeddable/public'; import { NavAction, SavedDashboardPanel } from '../types'; @@ -431,7 +432,7 @@ export class DashboardAppController { .getIncomingEmbeddablePackage(); if (incomingState) { if ('id' in incomingState) { - container.addNewEmbeddable(incomingState.type, { + container.addOrUpdateEmbeddable(incomingState.type, { savedObjectId: incomingState.id, }); } else if ('input' in incomingState) { @@ -440,7 +441,7 @@ export class DashboardAppController { const explicitInput = { savedVis: input, }; - container.addNewEmbeddable(incomingState.type, explicitInput); + container.addOrUpdateEmbeddable(incomingState.type, explicitInput); } } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index f1ecd0f221926..ff74580ba256b 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -46,7 +46,7 @@ import { } from '../../../../kibana_react/public'; import { PLACEHOLDER_EMBEDDABLE } from './placeholder'; import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement'; -import { EmbeddableStateTransfer } from '../../../../embeddable/public'; +import { EmbeddableStateTransfer, EmbeddableOutput } from '../../../../embeddable/public'; export interface DashboardContainerInput extends ContainerInput { viewMode: ViewMode; @@ -159,29 +159,55 @@ export class DashboardContainer extends Container) => { - const finalPanels = { ...this.input.panels }; - delete finalPanels[placeholderPanelState.explicitInput.id]; - const newPanelId = newPanelState.explicitInput?.id - ? newPanelState.explicitInput.id - : uuid.v4(); - finalPanels[newPanelId] = { - ...placeholderPanelState, - ...newPanelState, - gridData: { - ...placeholderPanelState.gridData, - i: newPanelId, - }, + newStateComplete.then((newPanelState: Partial) => + this.replacePanel(placeholderPanelState, newPanelState) + ); + } + + public replacePanel( + previousPanelState: DashboardPanelState, + newPanelState: Partial + ) { + // TODO: In the current infrastructure, embeddables in a container do not react properly to + // changes. Removing the existing embeddable, and adding a new one is a temporary workaround + // until the container logic is fixed. + const finalPanels = { ...this.input.panels }; + delete finalPanels[previousPanelState.explicitInput.id]; + const newPanelId = newPanelState.explicitInput?.id ? newPanelState.explicitInput.id : uuid.v4(); + finalPanels[newPanelId] = { + ...previousPanelState, + ...newPanelState, + gridData: { + ...previousPanelState.gridData, + i: newPanelId, + }, + explicitInput: { + ...newPanelState.explicitInput, + id: newPanelId, + }, + }; + this.updateInput({ + panels: finalPanels, + lastReloadRequestTime: new Date().getTime(), + }); + } + + public async addOrUpdateEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + EEO extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable + >(type: string, explicitInput: Partial) { + if (explicitInput.id && this.input.panels[explicitInput.id]) { + this.replacePanel(this.input.panels[explicitInput.id], { + type, explicitInput: { - ...newPanelState.explicitInput, - id: newPanelId, + ...explicitInput, + id: uuid.v4(), }, - }; - this.updateInput({ - panels: finalPanels, - lastReloadRequestTime: new Date().getTime(), }); - }); + } else { + this.addNewEmbeddable(type, explicitInput); + } } public render(dom: HTMLElement) { diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 8ec72dc1f9a74..22db1552e4303 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -44,7 +44,8 @@ export const UI_SETTINGS = { FORMAT_NUMBER_DEFAULT_LOCALE: 'format:number:defaultLocale', TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: 'timepicker:refreshIntervalDefaults', TIMEPICKER_QUICK_RANGES: 'timepicker:quickRanges', + TIMEPICKER_TIME_DEFAULTS: 'timepicker:timeDefaults', INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', -}; +} as const; diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts index c1aa2efe46998..18048b81aab96 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts @@ -78,7 +78,12 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record { + constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {} + + public async unwrapAttributes(input: RefType | ValType): Promise { + if (isSavedObjectEmbeddableInput(input)) { + const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< + SavedObjectAttributes + >(this.type, input.savedObjectId); + return savedObject.attributes; + } + return input.attributes; + } + + public async wrapAttributes( + newAttributes: SavedObjectAttributes, + useRefType: boolean, + embeddable?: IEmbeddable + ): Promise> { + const savedObjectId = + embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) + ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId + : undefined; + + if (useRefType) { + if (savedObjectId) { + await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); + return { savedObjectId } as RefType; + } else { + const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); + return { savedObjectId: savedItem.id } as RefType; + } + } else { + return { attributes: newAttributes } as ValType; + } + } +} diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 5bab5ac27f3cc..06cb6e322acf3 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -25,4 +25,5 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; export * from './saved_object_embeddable'; +export { AttributeService } from './attribute_service'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts index 6ca1800b16de4..5f093c55e94e4 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts @@ -26,5 +26,5 @@ export interface SavedObjectEmbeddableInput extends EmbeddableInput { export function isSavedObjectEmbeddableInput( input: EmbeddableInput | SavedObjectEmbeddableInput ): input is SavedObjectEmbeddableInput { - return (input as SavedObjectEmbeddableInput).savedObjectId !== undefined; + return Boolean((input as SavedObjectEmbeddableInput).savedObjectId); } diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index efd0ccdc4553d..48e5483124704 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -99,6 +99,7 @@ const createStartContract = (): Start => { getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), EmbeddablePanel: jest.fn(), + getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), filtersAndTimeRangeFromContext: jest.fn(), diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 03bb4a4779267..508c82c4247ed 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -43,11 +43,13 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + SavedObjectEmbeddableInput, ChartActionContext, isRangeSelectTriggerContext, isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; +import { AttributeService } from './lib/embeddables/attribute_service'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { @@ -82,6 +84,13 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; + getAttributeService: < + A, + V extends EmbeddableInput & { attributes: A }, + R extends SavedObjectEmbeddableInput + >( + type: string + ) => AttributeService; /** * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. @@ -206,6 +215,7 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), filtersFromContext, filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => { diff --git a/src/plugins/es_ui_shared/static/forms/components/form_row.tsx b/src/plugins/es_ui_shared/static/forms/components/form_row.tsx index ad5a517e40cfb..d38e6c4f5fd95 100644 --- a/src/plugins/es_ui_shared/static/forms/components/form_row.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/form_row.tsx @@ -57,13 +57,9 @@ export const FormRow = ({ titleWrapped = title; } - if (!children && !field) { - throw new Error('You need to provide either children or a field to the FormRow'); - } - return ( - {children ? children : } + {children ? children : field ? : null} ); }; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap index 6a2fd1000e6b4..0c89fb494b6ac 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap @@ -49,7 +49,7 @@ exports[`Header should render a different name, prompt, and beta tag if provided >

multiple - data souces, + data sources,

multiple - data souces, + data sources,

multiple - data souces, + data sources, multiple, single: filebeat-4-3-22, diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js similarity index 73% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js index 6790c49691dfd..d8d5087f8c380 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js @@ -19,11 +19,15 @@ import d3 from 'd3'; import _ from 'lodash'; -import expect from '@kbn/expect'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../test_utils/public'; -import { ChartTitle } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/chart_title'; -import { VisConfig } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { ChartTitle } from './chart_title'; +import { VisConfig } from './vis_config'; +import { getMockUiState } from '../../fixtures/mocks'; describe('Vislib ChartTitle Class Test Suite', function () { let mockUiState; @@ -88,6 +92,16 @@ describe('Vislib ChartTitle Class Test Suite', function () { yAxisLabel: 'Count', }; + let mockedHTMLElementClientSizes; + let mockedSVGElementGetBBox; + let mockedSVGElementGetComputedTextLength; + + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { mockUiState = getMockUiState(); el = d3.select('body').append('div').attr('class', 'visWrapper').datum(data); @@ -113,23 +127,29 @@ describe('Vislib ChartTitle Class Test Suite', function () { el.remove(); }); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('render Method', function () { beforeEach(function () { chartTitle.render(); }); - it('should append an svg to div', function () { - expect(el.select('.chart-title').selectAll('svg').length).to.be(1); + test('should append an svg to div', function () { + expect(el.select('.chart-title').selectAll('svg').length).toBe(1); }); - it('should append text', function () { - expect(!!el.select('.chart-title').selectAll('svg').selectAll('text')).to.be(true); + test('should append text', function () { + expect(!!el.select('.chart-title').selectAll('svg').selectAll('text')).toBe(true); }); }); describe('draw Method', function () { - it('should be a function', function () { - expect(_.isFunction(chartTitle.draw())).to.be(true); + test('should be a function', function () { + expect(_.isFunction(chartTitle.draw())).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js similarity index 67% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js index 20281d8479ab4..9c714af4d8434 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js @@ -19,13 +19,21 @@ import _ from 'lodash'; import d3 from 'd3'; -import expect from '@kbn/expect'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../test_utils/public'; // Data -import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import data from '../../fixtures/mock_data/date_histogram/_series'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { getVis } from '../_vis_fixture'; +import { getMockUiState } from '../../fixtures/mocks'; +import { getVis } from '../visualizations/_vis_fixture'; + +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; describe('Vislib Dispatch Class Test Suite', function () { function destroyVis(vis) { @@ -36,6 +44,18 @@ describe('Vislib Dispatch Class Test Suite', function () { return d3.select(element).data(new Array(n)).enter().append(type); } + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('', function () { let vis; let mockUiState; @@ -50,13 +70,13 @@ describe('Vislib Dispatch Class Test Suite', function () { destroyVis(vis); }); - it('implements on, off, emit methods', function () { + test('implements on, off, emit methods', function () { const events = _.map(vis.handler.charts, 'events'); - expect(events.length).to.be.above(0); + expect(events.length).toBeGreaterThan(0); events.forEach(function (dispatch) { - expect(dispatch).to.have.property('on'); - expect(dispatch).to.have.property('off'); - expect(dispatch).to.have.property('emit'); + expect(dispatch).toHaveProperty('on'); + expect(dispatch).toHaveProperty('off'); + expect(dispatch).toHaveProperty('emit'); }); }); }); @@ -77,15 +97,15 @@ describe('Vislib Dispatch Class Test Suite', function () { }); describe('addEvent method', function () { - it('returns a function that binds the passed event to a selection', function () { + test('returns a function that binds the passed event to a selection', function () { const chart = _.first(vis.handler.charts); const apply = chart.events.addEvent('event', _.noop); - expect(apply).to.be.a('function'); + expect(apply).toBeInstanceOf(Function); const els = getEls(vis.element, 3, 'div'); apply(els); els.each(function () { - expect(d3.select(this).on('event')).to.be(_.noop); + expect(d3.select(this).on('event')).toBe(_.noop); }); }); }); @@ -94,21 +114,21 @@ describe('Vislib Dispatch Class Test Suite', function () { // checking that they return function which bind the events expected function checkBoundAddMethod(name, event) { describe(name + ' method', function () { - it('should be a function', function () { + test('should be a function', function () { vis.handler.charts.forEach(function (chart) { - expect(chart.events[name]).to.be.a('function'); + expect(chart.events[name]).toBeInstanceOf(Function); }); }); - it('returns a function that binds ' + event + ' events to a selection', function () { + test('returns a function that binds ' + event + ' events to a selection', function () { const chart = _.first(vis.handler.charts); const apply = chart.events[name](chart.series[0].chartEl); - expect(apply).to.be.a('function'); + expect(apply).toBeInstanceOf(Function); const els = getEls(vis.element, 3, 'div'); apply(els); els.each(function () { - expect(d3.select(this).on(event)).to.be.a('function'); + expect(d3.select(this).on(event)).toBeInstanceOf(Function); }); }); }); @@ -119,26 +139,26 @@ describe('Vislib Dispatch Class Test Suite', function () { checkBoundAddMethod('addClickEvent', 'click'); describe('addMousePointer method', function () { - it('should be a function', function () { + test('should be a function', function () { vis.handler.charts.forEach(function (chart) { const pointer = chart.events.addMousePointer; - expect(_.isFunction(pointer)).to.be(true); + expect(_.isFunction(pointer)).toBe(true); }); }); }); describe('clickEvent handler', () => { describe('for pie chart', () => { - it('prepares data points', () => { + test('prepares data points', () => { const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }]; const d = { rawData: { column: 0, row: 0, table: {}, value: 0 } }; const chart = _.first(vis.handler.charts); const response = chart.events.clickEventResponse(d, { isSlices: true }); - expect(response.data).to.eql(expectedResponse); + expect(response.data).toEqual(expectedResponse); }); - it('remove invalid points', () => { + test('remove invalid points', () => { const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }]; const d = { rawData: { column: 0, row: 0, table: {}, value: 0 }, @@ -146,20 +166,20 @@ describe('Vislib Dispatch Class Test Suite', function () { }; const chart = _.first(vis.handler.charts); const response = chart.events.clickEventResponse(d, { isSlices: true }); - expect(response.data).to.eql(expectedResponse); + expect(response.data).toEqual(expectedResponse); }); }); describe('for xy charts', () => { - it('prepares data points', () => { + test('prepares data points', () => { const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }]; const d = { xRaw: { column: 0, row: 0, table: {}, value: 0 } }; const chart = _.first(vis.handler.charts); const response = chart.events.clickEventResponse(d, { isSlices: false }); - expect(response.data).to.eql(expectedResponse); + expect(response.data).toEqual(expectedResponse); }); - it('remove invalid points', () => { + test('remove invalid points', () => { const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }]; const d = { xRaw: { column: 0, row: 0, table: {}, value: 0 }, @@ -167,35 +187,35 @@ describe('Vislib Dispatch Class Test Suite', function () { }; const chart = _.first(vis.handler.charts); const response = chart.events.clickEventResponse(d, { isSlices: false }); - expect(response.data).to.eql(expectedResponse); + expect(response.data).toEqual(expectedResponse); }); }); }); }); describe('Custom event handlers', function () { - it('should attach whatever gets passed on vis.on() to chart.events', function (done) { + test('should attach whatever gets passed on vis.on() to chart.events', function (done) { const vis = getVis(); const mockUiState = getMockUiState(); vis.on('someEvent', _.noop); vis.render(data, mockUiState); vis.handler.charts.forEach(function (chart) { - expect(chart.events.listenerCount('someEvent')).to.be(1); + expect(chart.events.listenerCount('someEvent')).toBe(1); }); destroyVis(vis); done(); }); - it('can be added after rendering', function () { + test('can be added after rendering', function () { const vis = getVis(); const mockUiState = getMockUiState(); vis.render(data, mockUiState); vis.on('someEvent', _.noop); vis.handler.charts.forEach(function (chart) { - expect(chart.events.listenerCount('someEvent')).to.be(1); + expect(chart.events.listenerCount('someEvent')).toBe(1); }); destroyVis(vis); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js similarity index 58% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js rename to src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js index e4f75c47e621c..d50c70de1bb48 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js @@ -17,25 +17,38 @@ * under the License. */ -import expect from '@kbn/expect'; import $ from 'jquery'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../test_utils/public'; // Data -import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; -import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; -import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { getVis } from '../../_vis_fixture'; +import series from '../../fixtures/mock_data/date_histogram/_series'; +import columns from '../../fixtures/mock_data/date_histogram/_columns'; +import rows from '../../fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../fixtures/mocks'; +import { getVis } from '../visualizations/_vis_fixture'; const dateHistogramArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; dateHistogramArray.forEach(function (data, i) { describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function () { const events = ['click', 'brush']; let vis; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { vis = getVis(); vis.render(data, getMockUiState()); @@ -45,11 +58,17 @@ dateHistogramArray.forEach(function (data, i) { vis.destroy(); }); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('render Method', function () { - it('should render charts', function () { - expect(vis.handler.charts.length).to.be.greaterThan(0); + test('should render charts', function () { + expect(vis.handler.charts.length).toBeGreaterThan(0); vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('svg').length).to.be(1); + expect($(chart.chartEl).find('svg').length).toBe(1); }); }); }); @@ -67,10 +86,10 @@ dateHistogramArray.forEach(function (data, i) { }); }); - it('should add events to chart and emit to the Events class', function () { + test('should add events to chart and emit to the Events class', function () { charts.forEach(function (chart) { events.forEach(function (event) { - expect(chart.events.listenerCount(event)).to.be.above(0); + expect(chart.events.listenerCount(event)).toBeGreaterThan(0); }); }); }); @@ -89,10 +108,10 @@ dateHistogramArray.forEach(function (data, i) { }); }); - it('should remove events from the chart', function () { + test('should remove events from the chart', function () { charts.forEach(function (chart) { events.forEach(function (event) { - expect(chart.events.listenerCount(event)).to.be(0); + expect(chart.events.listenerCount(event)).toBe(0); }); }); }); @@ -103,8 +122,8 @@ dateHistogramArray.forEach(function (data, i) { vis.handler.removeAll(vis.element); }); - it('should remove all DOM elements from the el', function () { - expect($(vis.element).children().length).to.be(0); + test('should remove all DOM elements from the el', function () { + expect($(vis.element).children().length).toBe(0); }); }); @@ -113,9 +132,9 @@ dateHistogramArray.forEach(function (data, i) { vis.handler.error('This is an error!'); }); - it('should return an error classed DOM element with a text message', function () { - expect($(vis.element).find('.error').length).to.be(1); - expect($('.error h4').html()).to.be('This is an error!'); + test('should return an error classed DOM element with a text message', function () { + expect($(vis.element).find('.error').length).toBe(1); + expect($('.error h4').html()).toBe('This is an error!'); }); }); @@ -124,21 +143,21 @@ dateHistogramArray.forEach(function (data, i) { vis.handler.destroy(); }); - it('should destroy all the charts in the visualization', function () { - expect(vis.handler.charts.length).to.be(0); + test('should destroy all the charts in the visualization', function () { + expect(vis.handler.charts.length).toBe(0); }); }); describe('event proxying', function () { - it('should only pass the original event object to downstream handlers', function (done) { + test('should only pass the original event object to downstream handlers', function (done) { const event = {}; const chart = vis.handler.charts[0]; const mockEmitter = function () { const args = Array.from(arguments); - expect(args.length).to.be(2); - expect(args[0]).to.be('click'); - expect(args[1]).to.be(event); + expect(args.length).toBe(2); + expect(args[0]).toBe('click'); + expect(args[1]).toBe(event); done(); }; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js similarity index 55% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js index 7ad962fefc341..824d7073d6db5 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js @@ -18,22 +18,30 @@ */ import d3 from 'd3'; -import expect from '@kbn/expect'; import $ from 'jquery'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../../test_utils/public'; // Data -import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; -import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; -import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { Layout } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/layout/layout'; -import { VisConfig } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config'; -import { getVis } from '../../_vis_fixture'; +import series from '../../../fixtures/mock_data/date_histogram/_series'; +import columns from '../../../fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../fixtures/mocks'; +import { Layout } from './layout'; +import { VisConfig } from '../vis_config'; +import { getVis } from '../../visualizations/_vis_fixture'; const dateHistogramArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; + dateHistogramArray.forEach(function (data, i) { describe('Vislib Layout Class Test Suite for ' + names[i] + ' Data', function () { let vis; @@ -41,6 +49,12 @@ dateHistogramArray.forEach(function (data, i) { let numberOfCharts; let testLayout; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { vis = getVis(); mockUiState = getMockUiState(); @@ -52,19 +66,25 @@ dateHistogramArray.forEach(function (data, i) { vis.destroy(); }); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('createLayout Method', function () { - it('should append all the divs', function () { - expect($(vis.element).find('.visWrapper').length).to.be(1); - expect($(vis.element).find('.visAxis--y').length).to.be(2); - expect($(vis.element).find('.visWrapper__column').length).to.be(1); - expect($(vis.element).find('.visAxis__column--y').length).to.be(2); - expect($(vis.element).find('.y-axis-title').length).to.be.above(0); - expect($(vis.element).find('.visAxis__splitAxes--y').length).to.be(2); - expect($(vis.element).find('.visAxis__spacer--y').length).to.be(4); - expect($(vis.element).find('.visWrapper__chart').length).to.be(numberOfCharts); - expect($(vis.element).find('.visAxis--x').length).to.be(2); - expect($(vis.element).find('.visAxis__splitAxes--x').length).to.be(2); - expect($(vis.element).find('.x-axis-title').length).to.be.above(0); + test('should append all the divs', function () { + expect($(vis.element).find('.visWrapper').length).toBe(1); + expect($(vis.element).find('.visAxis--y').length).toBe(2); + expect($(vis.element).find('.visWrapper__column').length).toBe(1); + expect($(vis.element).find('.visAxis__column--y').length).toBe(2); + expect($(vis.element).find('.y-axis-title').length).toBeGreaterThan(0); + expect($(vis.element).find('.visAxis__splitAxes--y').length).toBe(2); + expect($(vis.element).find('.visAxis__spacer--y').length).toBe(4); + expect($(vis.element).find('.visWrapper__chart').length).toBe(numberOfCharts); + expect($(vis.element).find('.visAxis--x').length).toBe(2); + expect($(vis.element).find('.visAxis__splitAxes--x').length).toBe(2); + expect($(vis.element).find('.x-axis-title').length).toBeGreaterThan(0); }); }); @@ -82,44 +102,44 @@ dateHistogramArray.forEach(function (data, i) { testLayout = new Layout(visConfig); }); - it('should append a div with the correct class name', function () { - expect($(vis.element).find('.chart').length).to.be(numberOfCharts); + test('should append a div with the correct class name', function () { + expect($(vis.element).find('.chart').length).toBe(numberOfCharts); }); - it('should bind data to the DOM element', function () { - expect(!!$(vis.element).find('.chart').data()).to.be(true); + test('should bind data to the DOM element', function () { + expect(!!$(vis.element).find('.chart').data()).toBe(true); }); - it('should create children', function () { - expect(typeof $(vis.element).find('.x-axis-div')).to.be('object'); + test('should create children', function () { + expect(typeof $(vis.element).find('.x-axis-div')).toBe('object'); }); - it('should call split function when provided', function () { - expect(typeof $(vis.element).find('.x-axis-div')).to.be('object'); + test('should call split function when provided', function () { + expect(typeof $(vis.element).find('.x-axis-div')).toBe('object'); }); - it('should throw errors when incorrect arguments provided', function () { + test('should throw errors when incorrect arguments provided', function () { expect(function () { testLayout.layout({ parent: vis.element, type: undefined, class: 'chart', }); - }).to.throwError(); + }).toThrowError(); expect(function () { testLayout.layout({ type: 'div', class: 'chart', }); - }).to.throwError(); + }).toThrowError(); expect(function () { testLayout.layout({ parent: 'histogram', type: 'div', }); - }).to.throwError(); + }).toThrowError(); expect(function () { testLayout.layout({ @@ -129,7 +149,7 @@ dateHistogramArray.forEach(function (data, i) { }, class: 'chart', }); - }).to.throwError(); + }).toThrowError(); }); }); @@ -139,9 +159,9 @@ dateHistogramArray.forEach(function (data, i) { vis.handler.layout.appendElem('.visChart', 'div', 'test'); }); - it('should append DOM element to el with a class name', function () { - expect(typeof $(vis.element).find('.column')).to.be('object'); - expect(typeof $(vis.element).find('.test')).to.be('object'); + test('should append DOM element to el with a class name', function () { + expect(typeof $(vis.element).find('.column')).toBe('object'); + expect(typeof $(vis.element).find('.test')).toBe('object'); }); }); @@ -151,8 +171,8 @@ dateHistogramArray.forEach(function (data, i) { vis.handler.layout.removeAll(vis.element); }); - it('should remove all DOM elements from the el', function () { - expect($(vis.element).children().length).to.be(0); + test('should remove all DOM elements from the el', function () { + expect($(vis.element).children().length).toBe(0); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.test.js similarity index 56% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js rename to src/plugins/vis_type_vislib/public/vislib/vis.test.js index 36decdc415ed8..0c4fac97ccb9c 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js +++ b/src/plugins/vis_type_vislib/public/vislib/vis.test.js @@ -19,18 +19,25 @@ import _ from 'lodash'; import $ from 'jquery'; -import expect from '@kbn/expect'; - -import series from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import columns from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; -import rows from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; -import { getMockUiState } from '../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { getVis } from './_vis_fixture'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../test_utils/public'; +import series from '../fixtures/mock_data/date_histogram/_series'; +import columns from '../fixtures/mock_data/date_histogram/_columns'; +import rows from '../fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../fixtures/mocks'; +import { getVis } from './visualizations/_vis_fixture'; const dataArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; + dataArray.forEach(function (data, i) { describe('Vislib Vis Test Suite for ' + names[i] + ' Data', function () { const beforeEvent = 'click'; @@ -40,6 +47,12 @@ dataArray.forEach(function (data, i) { let secondVis; let numberOfCharts; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { vis = getVis(); secondVis = getVis(); @@ -51,34 +64,40 @@ dataArray.forEach(function (data, i) { secondVis.destroy(); }); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('render Method', function () { beforeEach(function () { vis.render(data, mockUiState); numberOfCharts = vis.handler.charts.length; }); - it('should bind data to this object', function () { - expect(_.isObject(vis.data)).to.be(true); + test('should bind data to this object', function () { + expect(_.isObject(vis.data)).toBe(true); }); - it('should instantiate a handler object', function () { - expect(_.isObject(vis.handler)).to.be(true); + test('should instantiate a handler object', function () { + expect(_.isObject(vis.handler)).toBe(true); }); - it('should append a chart', function () { - expect($('.chart').length).to.be(numberOfCharts); + test('should append a chart', function () { + expect($('.chart').length).toBe(numberOfCharts); }); - it('should throw an error if no data is provided', function () { + test('should throw an error if no data is provided', function () { expect(function () { vis.render(null, mockUiState); - }).to.throwError(); + }).toThrowError(); }); }); describe('getLegendColors method', () => { - it('should return null if no colors are defined', () => { - expect(vis.getLegendColors()).to.equal(null); + test('should return null if no colors are defined', () => { + expect(vis.getLegendColors()).toEqual(null); }); }); @@ -89,12 +108,12 @@ dataArray.forEach(function (data, i) { secondVis.destroy(); }); - it('should remove all DOM elements from el', function () { - expect($(secondVis.el).find('.visWrapper').length).to.be(0); + test('should remove all DOM elements from el', function () { + expect($(secondVis.el).find('.visWrapper').length).toBe(0); }); - it('should not remove visualizations that have not been destroyed', function () { - expect($(vis.element).find('.visWrapper').length).to.be(1); + test('should not remove visualizations that have not been destroyed', function () { + expect($(vis.element).find('.visWrapper').length).toBe(1); }); }); @@ -105,9 +124,9 @@ dataArray.forEach(function (data, i) { vis.set('offset', 'wiggle'); }); - it('should set an attribute', function () { - expect(vis.get('addLegend')).to.be(false); - expect(vis.get('offset')).to.be('wiggle'); + test('should set an attribute', function () { + expect(vis.get('addLegend')).toBe(false); + expect(vis.get('offset')).toBe('wiggle'); }); }); @@ -116,10 +135,10 @@ dataArray.forEach(function (data, i) { vis.render(data, mockUiState); }); - it('should get attribute values', function () { - expect(vis.get('addLegend')).to.be(true); - expect(vis.get('addTooltip')).to.be(true); - expect(vis.get('type')).to.be('point_series'); + test('should get attribute values', function () { + expect(vis.get('addLegend')).toBe(true); + expect(vis.get('addTooltip')).toBe(true); + expect(vis.get('type')).toBe('point_series'); }); }); @@ -148,22 +167,22 @@ dataArray.forEach(function (data, i) { vis.removeAllListeners(afterEvent); }); - it('should add an event and its listeners', function () { + test('should add an event and its listeners', function () { listeners.forEach(function (listener) { - expect(vis.listeners(beforeEvent)).to.contain(listener); + expect(vis.listeners(beforeEvent)).toContain(listener); }); listeners.forEach(function (listener) { - expect(vis.listeners(afterEvent)).to.contain(listener); + expect(vis.listeners(afterEvent)).toContain(listener); }); }); - it('should cause a listener for each event to be attached to each chart', function () { + test('should cause a listener for each event to be attached to each chart', function () { const charts = vis.handler.charts; charts.forEach(function (chart) { - expect(chart.events.listenerCount(beforeEvent)).to.be.above(0); - expect(chart.events.listenerCount(afterEvent)).to.be.above(0); + expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0); + expect(chart.events.listenerCount(afterEvent)).toBeGreaterThan(0); }); }); }); @@ -205,45 +224,45 @@ dataArray.forEach(function (data, i) { vis.removeAllListeners(afterEvent); }); - it('should remove a listener', function () { + test('should remove a listener', function () { const charts = vis.handler.charts; - expect(vis.listeners(beforeEvent)).to.not.contain(listener1); - expect(vis.listeners(beforeEvent)).to.contain(listener2); + expect(vis.listeners(beforeEvent)).not.toContain(listener1); + expect(vis.listeners(beforeEvent)).toContain(listener2); - expect(vis.listeners(afterEvent)).to.not.contain(listener1); - expect(vis.listeners(afterEvent)).to.contain(listener2); + expect(vis.listeners(afterEvent)).not.toContain(listener1); + expect(vis.listeners(afterEvent)).toContain(listener2); // Events should still be attached to charts charts.forEach(function (chart) { - expect(chart.events.listenerCount(beforeEvent)).to.be.above(0); - expect(chart.events.listenerCount(afterEvent)).to.be.above(0); + expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0); + expect(chart.events.listenerCount(afterEvent)).toBeGreaterThan(0); }); }); - it('should remove the event and all listeners when only event passed an argument', function () { + test('should remove the event and all listeners when only event passed an argument', function () { const charts = vis.handler.charts; vis.removeAllListeners(afterEvent); // should remove 'brush' event - expect(vis.listeners(beforeEvent)).to.contain(listener2); - expect(vis.listeners(afterEvent)).to.not.contain(listener2); + expect(vis.listeners(beforeEvent)).toContain(listener2); + expect(vis.listeners(afterEvent)).not.toContain(listener2); // should remove the event from the charts charts.forEach(function (chart) { - expect(chart.events.listenerCount(beforeEvent)).to.be.above(0); - expect(chart.events.listenerCount(afterEvent)).to.be(0); + expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0); + expect(chart.events.listenerCount(afterEvent)).toBe(0); }); }); - it('should remove the event from the chart when the last listener is removed', function () { + test('should remove the event from the chart when the last listener is removed', function () { const charts = vis.handler.charts; vis.off(afterEvent, listener2); - expect(vis.listenerCount(afterEvent)).to.be(0); + expect(vis.listenerCount(afterEvent)).toBe(0); charts.forEach(function (chart) { - expect(chart.events.listenerCount(afterEvent)).to.be(0); + expect(chart.events.listenerCount(afterEvent)).toBe(0); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js similarity index 83% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js index 7a68e847f13b1..0ffa53fc7ca9c 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js @@ -19,11 +19,10 @@ import _ from 'lodash'; import $ from 'jquery'; +import { coreMock } from '../../../../../core/public/mocks'; +import { chartPluginMock } from '../../../../charts/public/mocks'; -import { Vis } from '../../../../../../plugins/vis_type_vislib/public/vislib/vis'; - -// TODO: Remove when converted to jest mocks -import { ColorsService } from '../../../../../../plugins/charts/public/services'; +import { Vis } from '../vis'; const $visCanvas = $('

') .attr('id', 'vislib-vis-fixtures') @@ -55,15 +54,12 @@ afterEach(function () { }); const getDeps = () => { - const uiSettings = new Map(); - const colors = new ColorsService(); - colors.init(uiSettings); + const mockUiSettings = coreMock.createSetup().uiSettings; + const charts = chartPluginMock.createStartContract(); return { - uiSettings, - charts: { - colors, - }, + uiSettings: mockUiSettings, + charts: charts, }; }; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js similarity index 77% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js index 2b41ce5d1a5c6..94c9693819b69 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js @@ -18,11 +18,10 @@ */ import d3 from 'd3'; -import expect from '@kbn/expect'; - -import { Chart } from '../../../../../../../plugins/vis_type_vislib/public/vislib/visualizations/_chart'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { getVis } from '../_vis_fixture'; +import { setHTMLElementClientSizes, setSVGElementGetBBox } from '../../../../../test_utils/public'; +import { Chart } from './_chart'; +import { getMockUiState } from '../../fixtures/mocks'; +import { getVis } from './_vis_fixture'; describe('Vislib _chart Test Suite', function () { let vis; @@ -106,6 +105,14 @@ describe('Vislib _chart Test Suite', function () { yAxisLabel: 'Count', }; + let mockedHTMLElementClientSizes; + let mockedSVGElementGetBBox; + + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + }); + beforeEach(() => { el = d3.select('body').append('div').attr('class', 'column-chart'); @@ -127,11 +134,16 @@ describe('Vislib _chart Test Suite', function () { vis.destroy(); }); - it('should be a constructor for visualization modules', function () { - expect(myChart instanceof Chart).to.be(true); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + }); + + test('should be a constructor for visualization modules', function () { + expect(myChart instanceof Chart).toBe(true); }); - it('should have a render method', function () { - expect(typeof myChart.render === 'function').to.be(true); + test('should have a render method', function () { + expect(typeof myChart.render === 'function').toBe(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js similarity index 65% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js index 7c588800ae659..6fdc2a134b820 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js @@ -19,11 +19,11 @@ import $ from 'jquery'; import _ from 'lodash'; -import expect from '@kbn/expect'; +import { setHTMLElementClientSizes, setSVGElementGetBBox } from '../../../../../test_utils/public'; -import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { getVis } from '../_vis_fixture'; +import data from '../../fixtures/mock_data/terms/_series_multiple'; +import { getMockUiState } from '../../fixtures/mocks'; +import { getVis } from './_vis_fixture'; describe('Vislib Gauge Chart Test Suite', function () { let vis; @@ -82,6 +82,14 @@ describe('Vislib Gauge Chart Test Suite', function () { chartEl = vis.handler.charts[0].chartEl; } + let mockedHTMLElementClientSizes; + let mockedSVGElementGetBBox; + + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + }); + beforeEach(() => { generateVis(); }); @@ -91,55 +99,60 @@ describe('Vislib Gauge Chart Test Suite', function () { $('.visChart').remove(); }); - it('creates meter gauge', function () { - expect($(chartEl).find('svg').length).to.equal(5); - expect($(chartEl).find('svg > g > g > text').text()).to.equal('2820231918357341352'); + afterAll(function () { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + }); + + test('creates meter gauge', function () { + expect($(chartEl).find('svg').length).toEqual(5); + expect($(chartEl).find('svg > g > g > text').text()).toEqual('2820231918357341352'); }); - it('creates circle gauge', function () { + test('creates circle gauge', function () { generateVis({ gauge: { minAngle: 0, maxAngle: 2 * Math.PI, }, }); - expect($(chartEl).find('svg').length).to.equal(5); + expect($(chartEl).find('svg').length).toEqual(5); }); - it('creates gauge with automatic mode', function () { + test('creates gauge with automatic mode', function () { generateVis({ gauge: { alignment: 'automatic', }, }); - expect($(chartEl).find('svg').width()).to.equal(197); + expect($(chartEl).find('svg')[0].getAttribute('width')).toEqual('97'); }); - it('creates gauge with vertical mode', function () { + test('creates gauge with vertical mode', function () { generateVis({ gauge: { alignment: 'vertical', }, }); - expect($(chartEl).find('svg').width()).to.equal($(chartEl).width()); + expect($(chartEl).find('svg').width()).toEqual($(chartEl).width()); }); - it('applies range settings correctly', function () { + test('applies range settings correctly', function () { const paths = $(chartEl).find('svg > g > g:nth-child(1) > path:nth-child(2)'); const fills = []; paths.each(function () { fills.push(this.style.fill); }); - expect(fills).to.eql([ - 'rgb(165, 0, 38)', - 'rgb(255, 255, 190)', - 'rgb(255, 255, 190)', - 'rgb(0, 104, 55)', - 'rgb(0, 104, 55)', + expect(fills).toEqual([ + 'rgb(165,0,38)', + 'rgb(255,255,190)', + 'rgb(255,255,190)', + 'rgb(0,104,55)', + 'rgb(0,104,55)', ]); }); - it('applies color schema correctly', function () { + test('applies color schema correctly', function () { generateVis({ gauge: { colorSchema: 'Blues', @@ -150,12 +163,12 @@ describe('Vislib Gauge Chart Test Suite', function () { paths.each(function () { fills.push(this.style.fill); }); - expect(fills).to.eql([ - 'rgb(8, 48, 107)', - 'rgb(107, 174, 214)', - 'rgb(107, 174, 214)', - 'rgb(247, 251, 255)', - 'rgb(247, 251, 255)', + expect(fills).toEqual([ + 'rgb(8,48,107)', + 'rgb(107,174,214)', + 'rgb(107,174,214)', + 'rgb(247,251,255)', + 'rgb(247,251,255)', ]); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js similarity index 65% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js index d245905729c7e..e2da33d0808ba 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js @@ -20,16 +20,25 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; -import { getVis } from '../_vis_fixture'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../test_utils/public'; +import { getMockUiState } from '../../fixtures/mocks'; +import { getVis } from './_vis_fixture'; import { pieChartMockData } from './pie_chart_mock_data'; const names = ['rows', 'columns', 'slices']; const sizes = [0, 5, 15, 30, 60, 120]; +let mockedHTMLElementClientSizes = {}; +let mockWidth; +let mockHeight; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; + describe('No global chart settings', function () { const visLibParams1 = { el: '
', @@ -40,6 +49,14 @@ describe('No global chart settings', function () { let chart1; let mockUiState; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(120); + mockHeight = jest.spyOn($.prototype, 'height').mockReturnValue(120); + }); + beforeEach(() => { chart1 = getVis(visLibParams1); mockUiState = getMockUiState(); @@ -53,8 +70,16 @@ describe('No global chart settings', function () { chart1.destroy(); }); - it('should render chart titles for all charts', function () { - expect($(chart1.element).find('.visAxis__splitTitles--y').length).to.be(1); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); + mockHeight.mockRestore(); + }); + + test('should render chart titles for all charts', function () { + expect($(chart1.element).find('.visAxis__splitTitles--y').length).toBe(1); }); describe('_validatePieData method', function () { @@ -76,24 +101,54 @@ describe('No global chart settings', function () { { slices: { children: [{}] } }, ]; - it('should throw an error when all charts contain zeros', function () { + test('should throw an error when all charts contain zeros', function () { expect(function () { chart1.handler.ChartClass.prototype._validatePieData(allZeros); - }).to.throwError(); + }).toThrowError(); }); - it('should not throw an error when only some or no charts contain zeros', function () { + test('should not throw an error when only some or no charts contain zeros', function () { expect(function () { chart1.handler.ChartClass.prototype._validatePieData(someZeros); - }).to.not.throwError(); + }).not.toThrowError(); expect(function () { chart1.handler.ChartClass.prototype._validatePieData(noZeros); - }).to.not.throwError(); + }).not.toThrowError(); }); }); }); describe('Vislib PieChart Class Test Suite', function () { + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + let width = 120; + let height = 120; + const mockWidth = jest.spyOn($.prototype, 'width'); + mockWidth.mockImplementation((size) => { + if (size === undefined) { + return width; + } + width = size; + }); + const mockHeight = jest.spyOn($.prototype, 'height'); + mockHeight.mockImplementation((size) => { + if (size === undefined) { + return height; + } + height = size; + }); + }); + + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); + mockHeight.mockRestore(); + }); + ['rowData', 'columnData', 'sliceData'].forEach(function (aggItem, i) { describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () { const mockPieData = pieChartMockData[aggItem]; @@ -132,15 +187,15 @@ describe('Vislib PieChart Class Test Suite', function () { }); }); - it('should attach a click event', function () { + test('should attach a click event', function () { vis.handler.charts.forEach(function () { - expect(onClick).to.be(true); + expect(onClick).toBe(true); }); }); - it('should attach a hover event', function () { + test('should attach a hover event', function () { vis.handler.charts.forEach(function () { - expect(onMouseOver).to.be(true); + expect(onMouseOver).toBe(true); }); }); }); @@ -151,25 +206,25 @@ describe('Vislib PieChart Class Test Suite', function () { let svg; let slices; - it('should return an SVG object', function () { + test('should return an SVG object', function () { vis.handler.charts.forEach(function (chart) { $(chart.chartEl).find('svg').empty(); width = $(chart.chartEl).width(); height = $(chart.chartEl).height(); svg = d3.select($(chart.chartEl).find('svg')[0]); slices = chart.chartData.slices; - expect(_.isObject(chart.addPath(width, height, svg, slices))).to.be(true); + expect(_.isObject(chart.addPath(width, height, svg, slices))).toBe(true); }); }); - it('should draw path elements', function () { + test('should draw path elements', function () { vis.handler.charts.forEach(function (chart) { // test whether path elements are drawn - expect($(chart.chartEl).find('path').length).to.be.greaterThan(0); + expect($(chart.chartEl).find('path').length).toBeGreaterThan(0); }); }); - it('should draw labels', function () { + test('should draw labels', function () { vis.handler.charts.forEach(function (chart) { $(chart.chartEl).find('svg').empty(); width = $(chart.chartEl).width(); @@ -178,22 +233,22 @@ describe('Vislib PieChart Class Test Suite', function () { slices = chart.chartData.slices; chart._attr.labels.show = true; chart.addPath(width, height, svg, slices); - expect($(chart.chartEl).find('text.label-text').length).to.be.greaterThan(0); + expect($(chart.chartEl).find('text.label-text').length).toBeGreaterThan(0); }); }); }); describe('draw method', function () { - it('should return a function', function () { + test('should return a function', function () { vis.handler.charts.forEach(function (chart) { - expect(_.isFunction(chart.draw())).to.be(true); + expect(_.isFunction(chart.draw())).toBe(true); }); }); }); sizes.forEach(function (size) { describe('containerTooSmall error', function () { - it('should throw an error', function () { + test('should throw an error', function () { // 20px is the minimum height and width vis.handler.charts.forEach(function (chart) { $(chart.chartEl).height(size); @@ -202,12 +257,12 @@ describe('Vislib PieChart Class Test Suite', function () { if (size < 20) { expect(function () { chart.render(); - }).to.throwError(); + }).toThrowError(); } }); }); - it('should not throw an error', function () { + test('should not throw an error', function () { vis.handler.charts.forEach(function (chart) { $(chart.chartEl).height(size); $(chart.chartEl).width(size); @@ -215,7 +270,7 @@ describe('Vislib PieChart Class Test Suite', function () { if (size > 20) { expect(function () { chart.render(); - }).to.not.throwError(); + }).not.toThrowError(); } }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart_mock_data.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart_mock_data.js diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js similarity index 64% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js index fd2240c0c64c5..3cd58060978ee 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js @@ -20,18 +20,22 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import expect from '@kbn/expect'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../../test_utils/public'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getMockUiState } from '../../../fixtures/mocks'; import { getVis } from '../_vis_fixture'; const dataTypesArray = { - 'series pos': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'), - 'series pos neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'), - 'series neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'), - 'term columns': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'), - 'range rows': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'), - stackedSeries: require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'), + 'series pos': import('../../../fixtures/mock_data/date_histogram/_series'), + 'series pos neg': import('../../../fixtures/mock_data/date_histogram/_series_pos_neg'), + 'series neg': import('../../../fixtures/mock_data/date_histogram/_series_neg'), + 'term columns': import('../../../fixtures/mock_data/terms/_columns'), + 'range rows': import('../../../fixtures/mock_data/range/_rows'), + stackedSeries: import('../../../fixtures/mock_data/date_histogram/_stacked_series'), }; const visLibParams = { @@ -41,22 +45,38 @@ const visLibParams = { mode: 'stacked', }; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; + _.forOwn(dataTypesArray, function (dataType, dataTypeName) { describe('Vislib Area Chart Test Suite for ' + dataTypeName + ' Data', function () { let vis; let mockUiState; - beforeEach(() => { + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + + beforeEach(async () => { vis = getVis(visLibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); - vis.render(dataType, mockUiState); + vis.render(await dataType, mockUiState); }); afterEach(function () { vis.destroy(); }); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('stackData method', function () { let stackedData; let isStacked; @@ -73,15 +93,15 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { }); }); - it('should append a d.y0 key to the data object', function () { - expect(isStacked).to.be(true); + test('should append a d.y0 key to the data object', function () { + expect(isStacked).toBe(true); }); }); describe('addPath method', function () { - it('should append a area paths', function () { + test('should append a area paths', function () { vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('path').length).to.be.greaterThan(0); + expect($(chart.chartEl).find('path').length).toBeGreaterThan(0); }); }); }); @@ -101,9 +121,9 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { }); }); - it('should attach a hover event', function () { + test('should attach a hover event', function () { vis.handler.charts.forEach(function () { - expect(onMouseOver).to.be(true); + expect(onMouseOver).toBe(true); }); }); }); @@ -134,33 +154,33 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { // listeners, however, I was not able to test for the listener // function being present. I will need to update this test // in the future. - it('should attach a brush g element', function () { + test('should attach a brush g element', function () { vis.handler.charts.forEach(function () { - expect(onBrush).to.be(true); + expect(onBrush).toBe(true); }); }); - it('should attach a click event', function () { + test('should attach a click event', function () { vis.handler.charts.forEach(function () { - expect(onClick).to.be(true); + expect(onClick).toBe(true); }); }); - it('should attach a hover event', function () { + test('should attach a hover event', function () { vis.handler.charts.forEach(function () { - expect(onMouseOver).to.be(true); + expect(onMouseOver).toBe(true); }); }); }); describe('addCircles method', function () { - it('should append circles', function () { + test('should append circles', function () { vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0); + expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0); }); }); - it('should not draw circles where d.y === 0', function () { + test('should not draw circles where d.y === 0', function () { vis.handler.charts.forEach(function (chart) { const series = chart.chartData.series; const isZero = series.some(function (d) { @@ -172,80 +192,80 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { }); if (isZero) { - expect(isNotDrawn).to.be(false); + expect(isNotDrawn).toBe(false); } }); }); }); describe('draw method', function () { - it('should return a function', function () { + test('should return a function', function () { vis.handler.charts.forEach(function (chart) { - expect(_.isFunction(chart.draw())).to.be(true); + expect(_.isFunction(chart.draw())).toBe(true); }); }); - it('should return a yMin and yMax', function () { + test('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const domain = yAxis.getScale().domain(); - expect(domain[0]).to.not.be(undefined); - expect(domain[1]).to.not.be(undefined); + expect(domain[0]).not.toBe(undefined); + expect(domain[1]).not.toBe(undefined); }); }); - it('should render a zero axis line', function () { + test('should render a zero axis line', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; if (yAxis.yMin < 0 && yAxis.yMax > 0) { - expect($(chart.chartEl).find('line.zero-line').length).to.be(1); + expect($(chart.chartEl).find('line.zero-line').length).toBe(1); } }); }); }); describe('defaultYExtents is true', function () { - beforeEach(function () { + beforeEach(async function () { vis.visConfigArgs.defaultYExtents = true; - vis.render(dataType, mockUiState); + vis.render(await dataType, mockUiState); }); - it('should return yAxis extents equal to data extents', function () { + test('should return yAxis extents equal to data extents', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const min = vis.handler.valueAxes[0].axisScale.getYMin(); const max = vis.handler.valueAxes[0].axisScale.getYMax(); const domain = yAxis.getScale().domain(); - expect(domain[0]).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1]).toEqual(max); }); }); }); [0, 2, 4, 8].forEach(function (boundsMarginValue) { describe('defaultYExtents is true and boundsMargin is defined', function () { - beforeEach(function () { + beforeEach(async function () { vis.visConfigArgs.defaultYExtents = true; vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(dataType, mockUiState); + vis.render(await dataType, mockUiState); }); - it('should return yAxis extents equal to data extents with boundsMargin', function () { + test('should return yAxis extents equal to data extents with boundsMargin', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const min = vis.handler.valueAxes[0].axisScale.getYMin(); const max = vis.handler.valueAxes[0].axisScale.getYMax(); const domain = yAxis.getScale().domain(); if (min < 0 && max < 0) { - expect(domain[0]).to.equal(min); - expect(domain[1] - boundsMarginValue).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1] - boundsMarginValue).toEqual(max); } else if (min > 0 && max > 0) { - expect(domain[0] + boundsMarginValue).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0] + boundsMarginValue).toEqual(min); + expect(domain[1]).toEqual(max); } else { - expect(domain[0]).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1]).toEqual(max); } }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js similarity index 59% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js index 6b7ccaed25d49..f3d8d66df2d85 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js @@ -20,20 +20,24 @@ import _ from 'lodash'; import d3 from 'd3'; import $ from 'jquery'; -import expect from '@kbn/expect'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../../test_utils/public'; // Data -import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'; -import histogramRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows'; -import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; - -import { seriesMonthlyInterval } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval'; -import { rowsSeriesWithHoles } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes'; -import rowsWithZeros from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import series from '../../../fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg'; +import termsColumns from '../../../fixtures/mock_data/terms/_columns'; +import histogramRows from '../../../fixtures/mock_data/histogram/_rows'; +import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; + +import { seriesMonthlyInterval } from '../../../fixtures/mock_data/date_histogram/_series_monthly_interval'; +import { rowsSeriesWithHoles } from '../../../fixtures/mock_data/date_histogram/_rows_series_with_holes'; +import rowsWithZeros from '../../../fixtures/mock_data/date_histogram/_rows'; +import { getMockUiState } from '../../../fixtures/mocks'; import { getVis } from '../_vis_fixture'; // tuple, with the format [description, mode, data] @@ -46,6 +50,10 @@ const dataTypesArray = [ ['stackedSeries', 'stacked', stackedSeries], ]; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; + dataTypesArray.forEach(function (dataType) { const name = dataType[0]; const mode = dataType[1]; @@ -66,6 +74,12 @@ dataTypesArray.forEach(function (dataType) { }, }; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { vis = getVis(visLibParams); mockUiState = getMockUiState(); @@ -77,6 +91,12 @@ dataTypesArray.forEach(function (dataType) { vis.destroy(); }); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + describe('stackData method', function () { let stackedData; let isStacked; @@ -93,21 +113,21 @@ dataTypesArray.forEach(function (dataType) { }); }); - it('should stack values when mode is stacked', function () { + test('should stack values when mode is stacked', function () { if (mode === 'stacked') { - expect(isStacked).to.be(true); + expect(isStacked).toBe(true); } }); - it('should stack values when mode is percentage', function () { + test('should stack values when mode is percentage', function () { if (mode === 'percentage') { - expect(isStacked).to.be(true); + expect(isStacked).toBe(true); } }); }); describe('addBars method', function () { - it('should append rects', function () { + test('should append rects', function () { let numOfSeries; let numOfValues; let product; @@ -116,7 +136,7 @@ dataTypesArray.forEach(function (dataType) { numOfSeries = chart.chartData.series.length; numOfValues = chart.chartData.series[0].values.length; product = numOfSeries * numOfValues; - expect($(chart.chartEl).find('.series rect')).to.have.length(product); + expect($(chart.chartEl).find('.series rect')).toHaveLength(product); }); }); }); @@ -138,53 +158,53 @@ dataTypesArray.forEach(function (dataType) { }; } - it('should attach the brush if data is a set is ordered', function () { + test('should attach the brush if data is a set is ordered', function () { vis.handler.charts.forEach(function (chart) { const has = checkChart(chart); const ordered = vis.handler.data.get('ordered'); const allowBrushing = Boolean(ordered); - expect(has.brush).to.be(allowBrushing); + expect(has.brush).toBe(allowBrushing); }); }); - it('should attach a click event', function () { + test('should attach a click event', function () { vis.handler.charts.forEach(function (chart) { const has = checkChart(chart); - expect(has.click).to.be(true); + expect(has.click).toBe(true); }); }); - it('should attach a hover event', function () { + test('should attach a hover event', function () { vis.handler.charts.forEach(function (chart) { const has = checkChart(chart); - expect(has.mouseOver).to.be(true); + expect(has.mouseOver).toBe(true); }); }); }); describe('draw method', function () { - it('should return a function', function () { + test('should return a function', function () { vis.handler.charts.forEach(function (chart) { - expect(_.isFunction(chart.draw())).to.be(true); + expect(_.isFunction(chart.draw())).toBe(true); }); }); - it('should return a yMin and yMax', function () { + test('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const domain = yAxis.getScale().domain(); - expect(domain[0]).to.not.be(undefined); - expect(domain[1]).to.not.be(undefined); + expect(domain[0]).not.toBe(undefined); + expect(domain[1]).not.toBe(undefined); }); }); - it('should render a zero axis line', function () { + test('should render a zero axis line', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; if (yAxis.yMin < 0 && yAxis.yMax > 0) { - expect($(chart.chartEl).find('line.zero-line').length).to.be(1); + expect($(chart.chartEl).find('line.zero-line').length).toBe(1); } }); }); @@ -196,14 +216,14 @@ dataTypesArray.forEach(function (dataType) { vis.render(data, mockUiState); }); - it('should return yAxis extents equal to data extents', function () { + test('should return yAxis extents equal to data extents', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const min = vis.handler.valueAxes[0].axisScale.getYMin(); const max = vis.handler.valueAxes[0].axisScale.getYMax(); const domain = yAxis.getScale().domain(); - expect(domain[0]).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1]).toEqual(max); }); }); }); @@ -215,21 +235,21 @@ dataTypesArray.forEach(function (dataType) { vis.render(data, mockUiState); }); - it('should return yAxis extents equal to data extents with boundsMargin', function () { + test('should return yAxis extents equal to data extents with boundsMargin', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const min = vis.handler.valueAxes[0].axisScale.getYMin(); const max = vis.handler.valueAxes[0].axisScale.getYMax(); const domain = yAxis.getScale().domain(); if (min < 0 && max < 0) { - expect(domain[0]).to.equal(min); - expect(domain[1] - boundsMarginValue).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1] - boundsMarginValue).toEqual(max); } else if (min > 0 && max > 0) { - expect(domain[0] + boundsMarginValue).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0] + boundsMarginValue).toEqual(min); + expect(domain[1]).toEqual(max); } else { - expect(domain[0]).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1]).toEqual(max); } }); }); @@ -249,6 +269,12 @@ describe('stackData method - data set with zeros in percentage mode', function ( zeroFill: true, }; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { vis = getVis(visLibParams); mockUiState = getMockUiState(); @@ -259,29 +285,35 @@ describe('stackData method - data set with zeros in percentage mode', function ( vis.destroy(); }); - it('should not mutate the injected zeros', function () { + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + + test('should not mutate the injected zeros', function () { vis.render(seriesMonthlyInterval, mockUiState); - expect(vis.handler.charts).to.have.length(1); + expect(vis.handler.charts).toHaveLength(1); const chart = vis.handler.charts[0]; - expect(chart.chartData.series).to.have.length(1); + expect(chart.chartData.series).toHaveLength(1); const series = chart.chartData.series[0].values; // with the interval set in seriesMonthlyInterval data, the point at x=1454309600000 does not exist const point = _.find(series, ['x', 1454309600000]); - expect(point).to.not.be(undefined); - expect(point.y).to.be(0); + expect(point).not.toBe(undefined); + expect(point.y).toBe(0); }); - it('should not mutate zeros that exist in the data', function () { + test('should not mutate zeros that exist in the data', function () { vis.render(rowsWithZeros, mockUiState); - expect(vis.handler.charts).to.have.length(2); + expect(vis.handler.charts).toHaveLength(2); const chart = vis.handler.charts[0]; - expect(chart.chartData.series).to.have.length(5); + expect(chart.chartData.series).toHaveLength(5); const series = chart.chartData.series[0].values; const point = _.find(series, ['x', 1415826240000]); - expect(point).to.not.be(undefined); - expect(point.y).to.be(0); + expect(point).not.toBe(undefined); + expect(point.y).toBe(0); }); }); @@ -296,6 +328,12 @@ describe('datumWidth - split chart data set with holes', function () { zeroFill: true, }; + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + beforeEach(() => { vis = getVis(visLibParams); mockUiState = getMockUiState(); @@ -307,14 +345,20 @@ describe('datumWidth - split chart data set with holes', function () { vis.destroy(); }); - it('should not have bar widths that span multiple time bins', function () { - expect(vis.handler.charts.length).to.equal(1); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + + test('should not have bar widths that span multiple time bins', function () { + expect(vis.handler.charts.length).toEqual(1); const chart = vis.handler.charts[0]; const rects = $(chart.chartEl).find('.series rect'); const MAX_WIDTH_IN_PIXELS = 27; rects.each(function () { - const width = $(this).attr('width'); - expect(width).to.be.lessThan(MAX_WIDTH_IN_PIXELS); + const width = parseInt($(this).attr('width'), 10); + expect(width).toBeLessThan(MAX_WIDTH_IN_PIXELS); }); }); }); @@ -330,6 +374,15 @@ describe('datumWidth - monthly interval', function () { zeroFill: true, }; + let mockWidth; + + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); + }); + beforeEach(() => { vis = getVis(visLibParams); mockUiState = getMockUiState(); @@ -341,12 +394,19 @@ describe('datumWidth - monthly interval', function () { vis.destroy(); }); - it('should vary bar width when date histogram intervals are not equal', function () { - expect(vis.handler.charts.length).to.equal(1); + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); + }); + + test('should vary bar width when date histogram intervals are not equal', function () { + expect(vis.handler.charts.length).toEqual(1); const chart = vis.handler.charts[0]; const rects = $(chart.chartEl).find('.series rect'); - const januaryBarWidth = $(rects.get(0)).attr('width'); - const februaryBarWidth = $(rects.get(1)).attr('width'); - expect(februaryBarWidth).to.be.lessThan(januaryBarWidth); + const januaryBarWidth = parseInt($(rects.get(0)).attr('width'), 10); + const februaryBarWidth = parseInt($(rects.get(1)).attr('width'), 10); + expect(februaryBarWidth).toBeLessThan(januaryBarWidth); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js similarity index 65% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js index 9fa51fb59ed48..8c727d225c6c3 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js @@ -20,15 +20,19 @@ import _ from 'lodash'; import $ from 'jquery'; import d3 from 'd3'; -import expect from '@kbn/expect'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../../test_utils/public'; // Data -import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'; -import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import series from '../../../fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg'; +import termsColumns from '../../../fixtures/mock_data/terms/_columns'; +import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../fixtures/mocks'; import { getVis } from '../_vis_fixture'; // tuple, with the format [description, mode, data] @@ -40,7 +44,26 @@ const dataTypesArray = [ ['stackedSeries', stackedSeries], ]; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; +let mockWidth; + describe('Vislib Heatmap Chart Test Suite', function () { + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); + }); + + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); + }); + dataTypesArray.forEach(function (dataType) { const name = dataType[0]; const data = dataType[1]; @@ -76,7 +99,7 @@ describe('Vislib Heatmap Chart Test Suite', function () { vis.destroy(); }); - it('category axes should be rendered in reverse order', () => { + test('category axes should be rendered in reverse order', () => { const renderedCategoryAxes = vis.handler.renderArray.filter((item) => { return ( item.constructor && @@ -84,22 +107,22 @@ describe('Vislib Heatmap Chart Test Suite', function () { item.axisConfig.get('type') === 'category' ); }); - expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length); - expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal( + expect(vis.handler.categoryAxes.length).toEqual(renderedCategoryAxes.length); + expect(vis.handler.categoryAxes[0].axisConfig.get('id')).toEqual( renderedCategoryAxes[1].axisConfig.get('id') ); - expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal( + expect(vis.handler.categoryAxes[1].axisConfig.get('id')).toEqual( renderedCategoryAxes[0].axisConfig.get('id') ); }); describe('addSquares method', function () { - it('should append rects', function () { + test('should append rects', function () { vis.handler.charts.forEach(function (chart) { const numOfRects = chart.chartData.series.reduce((result, series) => { return result + series.values.length; }, 0); - expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects); + expect($(chart.chartEl).find('.series rect')).toHaveLength(numOfRects); }); }); }); @@ -120,53 +143,53 @@ describe('Vislib Heatmap Chart Test Suite', function () { }; } - it('should attach the brush if data is a set of ordered dates', function () { + test('should attach the brush if data is a set of ordered dates', function () { vis.handler.charts.forEach(function (chart) { const has = checkChart(chart); const ordered = vis.handler.data.get('ordered'); const date = Boolean(ordered && ordered.date); - expect(has.brush).to.be(date); + expect(has.brush).toBe(date); }); }); - it('should attach a click event', function () { + test('should attach a click event', function () { vis.handler.charts.forEach(function (chart) { const has = checkChart(chart); - expect(has.click).to.be(true); + expect(has.click).toBe(true); }); }); - it('should attach a hover event', function () { + test('should attach a hover event', function () { vis.handler.charts.forEach(function (chart) { const has = checkChart(chart); - expect(has.mouseOver).to.be(true); + expect(has.mouseOver).toBe(true); }); }); }); describe('draw method', function () { - it('should return a function', function () { + test('should return a function', function () { vis.handler.charts.forEach(function (chart) { - expect(_.isFunction(chart.draw())).to.be(true); + expect(_.isFunction(chart.draw())).toBe(true); }); }); - it('should return a yMin and yMax', function () { + test('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const domain = yAxis.getScale().domain(); - expect(domain[0]).to.not.be(undefined); - expect(domain[1]).to.not.be(undefined); + expect(domain[0]).not.toBe(undefined); + expect(domain[1]).not.toBe(undefined); }); }); }); - it('should define default colors', function () { - expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined); + test('should define default colors', function () { + expect(mockUiState.get('vis.defaultColors')).not.toBe(undefined); }); - it('should set custom range', function () { + test('should set custom range', function () { vis.destroy(); generateVis({ setColorRange: true, @@ -178,14 +201,14 @@ describe('Vislib Heatmap Chart Test Suite', function () { ], }); const labels = vis.getLegendLabels(); - expect(labels[0]).to.be('0 - 200'); - expect(labels[1]).to.be('200 - 400'); - expect(labels[2]).to.be('400 - 500'); - expect(labels[3]).to.be('500 - Infinity'); + expect(labels[0]).toBe('0 - 200'); + expect(labels[1]).toBe('200 - 400'); + expect(labels[2]).toBe('400 - 500'); + expect(labels[3]).toBe('500 - Infinity'); }); - it('should show correct Y axis title', function () { - expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal(''); + test('should show correct Y axis title', function () { + expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).toEqual(''); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js similarity index 67% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js index dae92c831cd8d..a84c74c095051 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js @@ -18,18 +18,22 @@ */ import d3 from 'd3'; -import expect from '@kbn/expect'; import $ from 'jquery'; import _ from 'lodash'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '../../../../../../test_utils/public'; // Data -import seriesPos from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; -import histogramColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns'; -import rangeRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'; -import termSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series'; -import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import seriesPos from '../../../fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg'; +import histogramColumns from '../../../fixtures/mock_data/histogram/_columns'; +import rangeRows from '../../../fixtures/mock_data/range/_rows'; +import termSeries from '../../../fixtures/mock_data/terms/_series'; +import { getMockUiState } from '../../../fixtures/mocks'; import { getVis } from '../_vis_fixture'; const dataTypes = [ @@ -41,7 +45,23 @@ const dataTypes = [ ['term series', termSeries], ]; +let mockedHTMLElementClientSizes; +let mockedSVGElementGetBBox; +let mockedSVGElementGetComputedTextLength; + describe('Vislib Line Chart', function () { + beforeAll(() => { + mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); + mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + }); + + afterAll(() => { + mockedHTMLElementClientSizes.mockRestore(); + mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + }); + dataTypes.forEach(function (type) { const name = type[0]; const data = type[1]; @@ -94,37 +114,37 @@ describe('Vislib Line Chart', function () { // listeners, however, I was not able to test for the listener // function being present. I will need to update this test // in the future. - it('should attach a brush g element', function () { + test('should attach a brush g element', function () { vis.handler.charts.forEach(function () { - expect(onBrush).to.be(true); + expect(onBrush).toBe(true); }); }); - it('should attach a click event', function () { + test('should attach a click event', function () { vis.handler.charts.forEach(function () { - expect(onClick).to.be(true); + expect(onClick).toBe(true); }); }); - it('should attach a hover event', function () { + test('should attach a hover event', function () { vis.handler.charts.forEach(function () { - expect(onMouseOver).to.be(true); + expect(onMouseOver).toBe(true); }); }); }); describe('addCircles method', function () { - it('should append circles', function () { + test('should append circles', function () { vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0); + expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0); }); }); }); describe('addLines method', function () { - it('should append a paths', function () { + test('should append a paths', function () { vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('path').length).to.be.greaterThan(0); + expect($(chart.chartEl).find('path').length).toBeGreaterThan(0); }); }); }); @@ -132,7 +152,7 @@ describe('Vislib Line Chart', function () { // Cannot seem to get these tests to work on the box // They however pass in the browsers //describe('addClipPath method', function () { - // it('should append a clipPath', function () { + // test('should append a clipPath', function () { // vis.handler.charts.forEach(function (chart) { // expect($(chart.chartEl).find('clipPath').length).to.be(1); // }); @@ -140,27 +160,27 @@ describe('Vislib Line Chart', function () { //}); describe('draw method', function () { - it('should return a function', function () { + test('should return a function', function () { vis.handler.charts.forEach(function (chart) { - expect(chart.draw()).to.be.a(Function); + expect(chart.draw()).toBeInstanceOf(Function); }); }); - it('should return a yMin and yMax', function () { + test('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const domain = yAxis.getScale().domain(); - expect(domain[0]).to.not.be(undefined); - expect(domain[1]).to.not.be(undefined); + expect(domain[0]).not.toBe(undefined); + expect(domain[1]).not.toBe(undefined); }); }); - it('should render a zero axis line', function () { + test('should render a zero axis line', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; if (yAxis.yMin < 0 && yAxis.yMax > 0) { - expect($(chart.chartEl).find('line.zero-line').length).to.be(1); + expect($(chart.chartEl).find('line.zero-line').length).toBe(1); } }); }); @@ -172,14 +192,14 @@ describe('Vislib Line Chart', function () { vis.render(data, mockUiState); }); - it('should return yAxis extents equal to data extents', function () { + test('should return yAxis extents equal to data extents', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const min = vis.handler.valueAxes[0].axisScale.getYMin(); const max = vis.handler.valueAxes[0].axisScale.getYMax(); const domain = yAxis.getScale().domain(); - expect(domain[0]).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1]).toEqual(max); }); }); }); @@ -191,21 +211,21 @@ describe('Vislib Line Chart', function () { vis.render(data, mockUiState); }); - it('should return yAxis extents equal to data extents with boundsMargin', function () { + test('should return yAxis extents equal to data extents with boundsMargin', function () { vis.handler.charts.forEach(function (chart) { const yAxis = chart.handler.valueAxes[0]; const min = vis.handler.valueAxes[0].axisScale.getYMin(); const max = vis.handler.valueAxes[0].axisScale.getYMax(); const domain = yAxis.getScale().domain(); if (min < 0 && max < 0) { - expect(domain[0]).to.equal(min); - expect(domain[1] - boundsMarginValue).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1] - boundsMarginValue).toEqual(max); } else if (min > 0 && max > 0) { - expect(domain[0] + boundsMarginValue).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0] + boundsMarginValue).toEqual(min); + expect(domain[1]).toEqual(max); } else { - expect(domain[0]).to.equal(min); - expect(domain[1]).to.equal(max); + expect(domain[0]).toEqual(min); + expect(domain[1]).toEqual(max); } }); }); diff --git a/src/test_utils/public/helpers/index.ts b/src/test_utils/public/helpers/index.ts index c8447743ee287..fcc0102c76683 100644 --- a/src/test_utils/public/helpers/index.ts +++ b/src/test_utils/public/helpers/index.ts @@ -25,4 +25,9 @@ export { WithMemoryRouter, WithRoute, reactRouterMock } from './router_helpers'; export * from './utils'; -export { setSVGElementGetBBox, setHTMLElementOffset } from './jsdom_svg_mocks'; +export { + setSVGElementGetBBox, + setHTMLElementOffset, + setHTMLElementClientSizes, + setSVGElementGetComputedTextLength, +} from './jsdom_svg_mocks'; diff --git a/src/test_utils/public/helpers/jsdom_svg_mocks.ts b/src/test_utils/public/helpers/jsdom_svg_mocks.ts index dbc8266f663f1..6ef4204baa2ff 100644 --- a/src/test_utils/public/helpers/jsdom_svg_mocks.ts +++ b/src/test_utils/public/helpers/jsdom_svg_mocks.ts @@ -17,6 +17,20 @@ * under the License. */ +export const setHTMLElementClientSizes = (width: number, height: number) => { + const spyWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get'); + spyWidth.mockReturnValue(width); + const spyHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get'); + spyHeight.mockReturnValue(height); + + return { + mockRestore: () => { + spyWidth.mockRestore(); + spyHeight.mockRestore(); + }, + }; +}; + export const setSVGElementGetBBox = ( width: number, height: number, @@ -41,6 +55,20 @@ export const setSVGElementGetBBox = ( }; }; +export const setSVGElementGetComputedTextLength = (width: number) => { + const SVGElementPrototype = SVGElement.prototype as any; + const originalGetComputedTextLength = SVGElementPrototype.getComputedTextLength; + + // getComputedTextLength is not in the SVGElement.prototype object by default, so we cannot use jest.spyOn for that case + SVGElementPrototype.getComputedTextLength = jest.fn(() => width); + + return { + mockRestore: () => { + SVGElementPrototype.getComputedTextLength = originalGetComputedTextLength; + }, + }; +}; + export const setHTMLElementOffset = (width: number, height: number) => { const offsetWidthSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get'); offsetWidthSpy.mockReturnValue(width); diff --git a/src/test_utils/public/index.ts b/src/test_utils/public/index.ts index 4f46dfe1578db..e57f1ae8ea7a9 100644 --- a/src/test_utils/public/index.ts +++ b/src/test_utils/public/index.ts @@ -17,4 +17,9 @@ * under the License. */ -export { setSVGElementGetBBox, setHTMLElementOffset } from './helpers'; +export { + setSVGElementGetBBox, + setHTMLElementOffset, + setHTMLElementClientSizes, + setSVGElementGetComputedTextLength, +} from './helpers'; diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 7c4f75bea8801..fa4bdc8ed2266 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -110,7 +110,7 @@ module.exports = function (grunt) { customLaunchers: { Chrome_Headless: { base: 'Chrome', - flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222', '--no-sandbox'], + flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222'], }, }, diff --git a/tasks/test_jest.js b/tasks/test_jest.js index 810ed42324840..d8f51806e8ddc 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'), ['--maxWorkers=10']).then(done, done); + runJest(resolve(__dirname, '../scripts/jest.js')).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, args = []) { + function runJest(jestScript) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci', ...args], + args: [jestScript, '--ci'], opts: { stdio: 'inherit' }, }; diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 949a01ff7873a..47741c1ab8a0d 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -96,25 +96,32 @@ export default function ({ getService, getPageObjects }) { it('should modify the time range when a bar is clicked', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.clickHistogramBar(); + await PageObjects.discover.waitUntilSearchingHasFinished(); const time = await PageObjects.timePicker.getTimeConfig(); expect(time.start).to.be('Sep 21, 2015 @ 09:00:00.000'); expect(time.end).to.be('Sep 21, 2015 @ 12:00:00.000'); - const rowData = await PageObjects.discover.getDocTableField(1); - expect(rowData).to.have.string('Sep 21, 2015 @ 11:59:22.316'); + await retry.waitFor('doc table to contain the right search result', async () => { + const rowData = await PageObjects.discover.getDocTableField(1); + log.debug(`The first timestamp value in doc table: ${rowData}`); + return rowData.includes('Sep 21, 2015 @ 11:59:22.316'); + }); }); it('should modify the time range when the histogram is brushed', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.brushHistogram(); + await PageObjects.discover.waitUntilSearchingHasFinished(); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); expect(Math.round(newDurationHours)).to.be(24); - const rowData = await PageObjects.discover.getDocTableField(1); - log.debug(`The first timestamp value in doc table: ${rowData}`); - expect(Date.parse(rowData)).to.be.within( - Date.parse('Sep 20, 2015 @ 17:30:00.000'), - Date.parse('Sep 20, 2015 @ 23:30:00.000') - ); + + await retry.waitFor('doc table to contain the right search result', async () => { + const rowData = await PageObjects.discover.getDocTableField(1); + log.debug(`The first timestamp value in doc table: ${rowData}`); + const dateParsed = Date.parse(rowData); + //compare against the parsed date of Sep 20, 2015 @ 17:30:00.000 and Sep 20, 2015 @ 23:30:00.000 + return dateParsed >= 1442770200000 && dateParsed <= 1442791800000; + }); }); it('should show correct initial chart interval of Auto', async function () { diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index f6a092ecb79a8..9bcf7fd2d73b5 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -20,14 +20,20 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const log = getService('log'); const docTable = getService('docTable'); + const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']); const esArchiver = getService('esArchiver'); const retry = getService('retry'); - describe('doc link in discover', function contextSize() { - before(async function () { + // Flaky: https://github.com/elastic/kibana/issues/71216 + describe.skip('doc link in discover', function contextSize() { + beforeEach(async function () { + log.debug('load kibana index with default index pattern'); + await esArchiver.loadIfNeeded('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -50,5 +56,27 @@ export default function ({ getService, getPageObjects }) { const hasDocHit = await testSubjects.exists('doc-hit'); expect(hasDocHit).to.be(true); }); + + it('add filter should create an exists filter if value is null (#7189)', async function () { + await PageObjects.discover.waitUntilSearchingHasFinished(); + // Filter special document + await filterBar.addFilter('agent', 'is', 'Missing/Fields'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + // navigate to the doc view + await docTable.clickRowToggle({ rowIndex: 0 }); + + const details = await docTable.getDetailsRow(); + await docTable.addInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const hasInclusiveFilter = await filterBar.hasFilter('referer', 'exists', true, false, true); + expect(hasInclusiveFilter).to.be(true); + + await docTable.removeInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); + expect(hasExcludeFilter).to.be(true); + }); }); } diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 61bb5f7cfee6f..6b423bf6eeb1d 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -20,6 +20,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const retry = getService('retry'); const log = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -93,7 +94,10 @@ export default function ({ getService, getPageObjects }) { expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); - expect(await PageObjects.discover.getHitCount()).to.be('2,792'); + await retry.waitFor( + 'the right hit count', + async () => (await PageObjects.discover.getHitCount()) === '2,792' + ); expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); }); @@ -149,7 +153,6 @@ export default function ({ getService, getPageObjects }) { expect(await queryBar.getQueryString()).to.eql(''); }); - // https://github.com/elastic/kibana/issues/63505 it('allows clearing if non default language was remembered in localstorage', async () => { await queryBar.switchQueryLanguage('lucene'); await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url @@ -160,9 +163,7 @@ export default function ({ getService, getPageObjects }) { await queryBar.expectQueryLanguageOrFail('lucene'); }); - // fails: bug in discover https://github.com/elastic/kibana/issues/63561 - // unskip this test when bug is fixed - it.skip('changing language removes saved query', async () => { + it('changing language removes saved query', async () => { await savedQueryManagementComponent.loadSavedQuery('OkResponse'); await queryBar.switchQueryLanguage('lucene'); expect(await queryBar.getQueryString()).to.eql(''); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js index d64629a65c2c3..fd06257a91ff4 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.js +++ b/test/functional/apps/visualize/_data_table_nontimeindex.js @@ -112,8 +112,7 @@ export default function ({ getService, getPageObjects }) { expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); }); - // bug https://github.com/elastic/kibana/issues/68977 - describe.skip('data table with date histogram', async () => { + describe('data table with date histogram', async () => { before(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); @@ -123,7 +122,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.clickBucket('Split rows'); await PageObjects.visEditor.selectAggregation('Date Histogram'); await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.setInterval('Day'); await PageObjects.visEditor.clickGo(); }); diff --git a/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz b/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz index a212c34e2ead6..a4f889da61128 100644 Binary files a/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz and b/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz differ diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index d058695ea6819..03d21aa4aa52f 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -87,13 +87,15 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv async waitTableIsLoaded() { return retry.try(async () => { - const exists = await find.existsByDisplayedByCssSelector( - '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading' + const isLoaded = await find.existsByDisplayedByCssSelector( + '*[data-test-subj="savedObjectsTable"] :not(.euiBasicTable-loading)' ); - if (exists) { + + if (isLoaded) { + return true; + } else { throw new Error('Waiting'); } - return true; }); } diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts index 52593de68705b..1ac8de69ee5f4 100644 --- a/test/functional/services/doc_table.ts +++ b/test/functional/services/doc_table.ts @@ -58,6 +58,11 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont : (await this.getBodyRows())[options.rowIndex]; } + public async getDetailsRow(): Promise { + const table = await this.getTable(); + return await table.findByCssSelector('[data-test-subj~="docTableDetailsRow"]'); + } + public async getAnchorDetailsRow(): Promise { const table = await this.getTable(); return await table.findByCssSelector( @@ -133,6 +138,22 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + public async getRemoveInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); + } + + public async removeInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: WebElementWrapper + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + public async getAddExistsFilterButton( tableDocViewRow: WebElementWrapper ): Promise { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index f6531f8d872c2..98ab1babd60fe 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -31,17 +31,21 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon * @param key field name * @param value filter value * @param enabled filter status + * @param pinned filter pinned status + * @param negated filter including or excluding value */ public async hasFilter( key: string, value: string, enabled: boolean = true, - pinned: boolean = false + pinned: boolean = false, + negated: boolean = false ): Promise { const filterActivationState = enabled ? 'enabled' : 'disabled'; const filterPinnedState = pinned ? 'pinned' : 'unpinned'; + const filterNegatedState = negated ? 'filter-negated' : ''; return testSubjects.exists( - `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState}`, + `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState} ${filterNegatedState}`, { allowHidden: true, } diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh deleted file mode 100755 index 503d12b2f6d73..0000000000000 --- a/test/scripts/checks/doc_api_changes.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 513664263791b..0000000000000 --- a/test/scripts/checks/file_casing.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 7a6fd46c46c76..0000000000000 --- a/test/scripts/checks/i18n.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index a08d7d07a24a1..0000000000000 --- a/test/scripts/checks/licenses.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 1d43d32c9feb8..0000000000000 --- a/test/scripts/checks/lock_file_symlinks.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:checkLockfileSymlinks diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh deleted file mode 100755 index 9184758577654..0000000000000 --- a/test/scripts/checks/test_hardening.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 5f9aafe80e10e..0000000000000 --- a/test/scripts/checks/test_projects.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index d667c753baec2..0000000000000 --- a/test/scripts/checks/ts_projects.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 07c49638134be..0000000000000 --- a/test/scripts/checks/type_check.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index b73a71e7ff7fd..0000000000000 --- a/test/scripts/checks/verify_dependency_versions.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 9f8343e540861..0000000000000 --- a/test/scripts/checks/verify_notice.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:verifyNotice diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh old mode 100755 new mode 100644 diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index f449986713f97..3e49edc8e6ae5 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,9 +2,19 @@ source src/dev/ci_setup/setup_env.sh -if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - ./test/scripts/jenkins_build_plugins.sh -fi +echo " -> building examples separate from test plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --examples \ + --verbose; + +echo " -> building test plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --no-examples \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --verbose; # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -16,7 +26,4 @@ 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 deleted file mode 100755 index 32b3942074b34..0000000000000 --- a/test/scripts/jenkins_build_plugins.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -echo " -> building examples separate from test plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --examples \ - --workers 6 \ - --verbose - -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --no-examples \ - --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 2542d7032e83b..60d7f0406f4c9 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 [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then + if [ "$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_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh deleted file mode 100755 index 1d691d98982de..0000000000000 --- a/test/scripts/jenkins_plugin_functional.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/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 100755 new mode 100644 index a5a1a2103801f..204911a3eedaa --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash -source test/scripts/jenkins_test_setup_xpack.sh +source test/scripts/jenkins_test_setup.sh + +installDir="$PARENT_DIR/install/kibana" +destDir="${installDir}-${CI_WORKER_NUMBER}" +cp -R "$installDir" "$destDir" + +export KIBANA_INSTALL_DIR="$destDir" echo " -> Running security solution cypress tests" cd "$XPACK_DIR" diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh deleted file mode 100755 index 5274d05572e71..0000000000000 --- a/test/scripts/jenkins_setup_parallel_workspace.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/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 100755 new mode 100644 index 7cced76eb650f..49ee8a6b526ca --- a/test/scripts/jenkins_test_setup.sh +++ b/test/scripts/jenkins_test_setup.sh @@ -14,7 +14,3 @@ trap 'post_work' EXIT export TEST_BROWSER_HEADLESS=1 source src/dev/ci_setup/setup_env.sh - -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 100755 new mode 100644 index b7eac33f35176..7bbb867526384 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -2,17 +2,10 @@ source test/scripts/jenkins_test_setup.sh -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 +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" 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 100755 new mode 100644 index 74a3de77e3a76..a72e9749ebbd5 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,18 +3,11 @@ 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" - 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)" + export KIBANA_INSTALL_DIR="$destDir" cd "$XPACK_DIR" fi diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 2452e2f5b8c58..58ef6a42d3fe4 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,9 +3,21 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh -if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - ./test/scripts/jenkins_xpack_build_plugins.sh -fi +echo " -> building examples separate from test plugins" +node scripts/build_kibana_platform_plugins \ + --examples \ + --verbose; + +echo " -> building test plugins" +node scripts/build_kibana_platform_plugins \ + --no-examples \ + --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; # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -30,10 +42,7 @@ 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="$KIBANA_DIR/install/kibana" + installDir="$PARENT_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 deleted file mode 100755 index fea30c547bd5f..0000000000000 --- a/test/scripts/jenkins_xpack_build_plugins.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -echo " -> building examples separate from test plugins" -node scripts/build_kibana_platform_plugins \ - --workers 12 \ - --examples \ - --verbose - -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --no-examples \ - --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_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh old mode 100755 new mode 100644 diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh deleted file mode 100755 index c3211300b96c5..0000000000000 --- a/test/scripts/lint/eslint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index b9c683bcb049e..0000000000000 --- a/test/scripts/lint/sasslint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 152c97a3ca7df..0000000000000 --- a/test/scripts/test/api_integration.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 73dbbddfb38f6..0000000000000 --- a/test/scripts/test/jest_integration.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index e25452698cebc..0000000000000 --- a/test/scripts/test/jest_unit.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index e9985300ba19d..0000000000000 --- a/test/scripts/test/karma_ci.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 deleted file mode 100755 index 43c00f0a09dcf..0000000000000 --- a/test/scripts/test/mocha.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:mocha diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh deleted file mode 100755 index 93d70ec355391..0000000000000 --- a/test/scripts/test/xpack_jest_unit.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 deleted file mode 100755 index 9078f01f1b870..0000000000000 --- a/test/scripts/test/xpack_karma.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 deleted file mode 100755 index 493fe9f58d322..0000000000000 --- a/test/scripts/test/xpack_list_cyclic_dependency.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 deleted file mode 100755 index b21301f25ad08..0000000000000 --- a/test/scripts/test/xpack_siem_cyclic_dependency.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 2a1b55d832606..460a90b8ec0c0 100644 --- a/vars/catchErrors.groovy +++ b/vars/catchErrors.groovy @@ -1,15 +1,8 @@ // 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) { - try { - closure() - } catch (ex) { - params.catchInterruptions = false - catchError(params) { - throw ex - } - } + params.catchInterruptions = false + return catchError(params, closure) } return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 0f11204311451..f3fc5f84583c9 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -16,34 +16,27 @@ def withPostBuildReporting(Closure closure) { } } -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) + return { processNumber -> + def kibanaPort = "61${processNumber}1" + def esPort = "61${processNumber}2" + def esTransportPort = "61${processNumber}3" + def ingestManagementPackageRegistryPort = "61${processNumber}4" + + 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", + ]) { + closure() + } } } @@ -107,17 +100,11 @@ def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', - 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', 'target/junit/**/*', - 'target/test-suites-ci-plan.json', - 'test/**/screenshots/session/*.png', - 'test/**/screenshots/failure/*.png', - 'test/**/screenshots/diff/*.png', + 'test/**/screenshots/**/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/session/*.png', - 'x-pack/test/**/screenshots/failure/*.png', - 'x-pack/test/**/screenshots/diff/*.png', + 'x-pack/test/**/screenshots/**/*.png', 'x-pack/test/functional/failure_debug/html/*.html', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] @@ -132,12 +119,6 @@ def withGcsArtifactUpload(workerName, closure) { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } - - dir(env.WORKSPACE) { - ARTIFACT_PATTERNS.each { pattern -> - uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}") - } - } } } }) @@ -150,11 +131,6 @@ def withGcsArtifactUpload(workerName, closure) { def publishJunit() { junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) - - // junit() is weird about paths for security reasons, so we need to actually change to an upper directory first - dir(env.WORKSPACE) { - junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) - } } def sendMail() { @@ -218,16 +194,12 @@ def doSetup() { } } -def buildOss(maxWorkers = '') { - withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") - } +def buildOss() { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } -def buildXpack(maxWorkers = '') { - withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") - } +def buildXpack() { + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } def runErrorReporter() { @@ -276,100 +248,6 @@ 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 { - 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) { @@ -380,4 +258,5 @@ def pipelineLibraryTests() { } } + return this diff --git a/vars/task.groovy b/vars/task.groovy deleted file mode 100644 index 0c07b519b6fef..0000000000000 --- a/vars/task.groovy +++ /dev/null @@ -1,5 +0,0 @@ -def call(Closure closure) { - withTaskQueue.addTask(closure) -} - -return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy deleted file mode 100644 index 3ff9a7b4850ae..0000000000000 --- a/vars/tasks.groovy +++ /dev/null @@ -1,124 +0,0 @@ -def call(List closures) { - withTaskQueue.addTasks(closures) -} - -def check() { - tasks([ - 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 4 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.scriptTaskDocker('Karma CI Tests', 'test/scripts/test/karma_ci.sh'), - kibanaPipeline.scriptTaskDocker('X-Pack Karma Tests', 'test/scripts/test/xpack_karma.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('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: !githubPr.isPr(), - 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: !githubPr.isPr(), - 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.pageLoadMetrics) { - task(kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.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/']) { - task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) - } - } -} - -return this diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy deleted file mode 100644 index 8132d6264744f..0000000000000 --- a/vars/withTaskQueue.groovy +++ /dev/null @@ -1,154 +0,0 @@ -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 2e94ce12f34c0..8b7e8525a7ce3 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -13,8 +13,6 @@ 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' } @@ -57,11 +55,6 @@ def base(Map params, Closure closure) { } } - sh( - script: "mkdir -p ${env.WORKSPACE}/tmp", - label: "Create custom temp directory" - ) - def checkoutInfo = [:] if (config.scm) { @@ -96,7 +89,6 @@ 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'), @@ -175,9 +167,7 @@ def parallelProcesses(Map params) { sleep(delay) } - withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) { - processClosure() - } + processClosure(processNumber) } } diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts new file mode 100644 index 0000000000000..1fd927d82f186 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ServiceAnomalyStats { + transactionType?: string; + anomalyScore?: number; + actualValue?: number; + jobId?: string; +} diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 43f3585d0ebb2..b50db270ef544 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -15,11 +15,13 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from './elasticsearch_fieldnames'; +import { ServiceAnomalyStats } from './anomaly_detection'; export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition { [SERVICE_NAME]: string; [SERVICE_ENVIRONMENT]: string | null; [AGENT_NAME]: string; + serviceAnomalyStats?: ServiceAnomalyStats; } export interface ExternalConnectionNode extends cytoscape.NodeDataDefinition { [SPAN_DESTINATION_SERVICE_RESOURCE]: string; @@ -37,8 +39,10 @@ export interface Connection { export interface ServiceNodeMetrics { avgMemoryUsage: number | null; avgCpuUsage: number | null; - avgTransactionDuration: number | null; - avgRequestsPerMinute: number | null; + transactionStats: { + avgTransactionDuration: number | null; + avgRequestsPerMinute: number | null; + }; avgErrorsPerMinute: number | null; } diff --git a/x-pack/plugins/apm/common/utils/range_filter.ts b/x-pack/plugins/apm/common/utils/range_filter.ts index 08062cbf76bc6..9ffec18d95fb0 100644 --- a/x-pack/plugins/apm/common/utils/range_filter.ts +++ b/x-pack/plugins/apm/common/utils/range_filter.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export function rangeFilter( - start: number, - end: number, - timestampField = '@timestamp' -) { +export function rangeFilter(start: number, end: number) { return { - [timestampField]: { + '@timestamp': { gte: start, lte: end, format: 'epoch_millis', diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx new file mode 100644 index 0000000000000..410ba8b5027fb --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiIconTip, + EuiHealth, +} from '@elastic/eui'; +import { useTheme } from '../../../../hooks/useTheme'; +import { fontSize, px } from '../../../../style/variables'; +import { asInteger, asDuration } from '../../../../utils/formatters'; +import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; +import { getSeverityColor, popoverWidth } from '../cytoscapeOptions'; +import { getSeverity } from '../../../../../common/ml_job_constants'; +import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; +import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; + +const HealthStatusTitle = styled(EuiTitle)` + display: inline; + text-transform: uppercase; +`; + +const VerticallyCentered = styled.div` + display: flex; + align-items: center; +`; + +const SubduedText = styled.span` + color: ${({ theme }) => theme.eui.euiTextSubduedColor}; +`; + +const EnableText = styled.section` + color: ${({ theme }) => theme.eui.euiTextSubduedColor}; + line-height: 1.4; + font-size: ${fontSize}; + width: ${px(popoverWidth)}; +`; + +export const ContentLine = styled.section` + line-height: 2; +`; + +interface Props { + serviceName: string; + serviceAnomalyStats: ServiceAnomalyStats | undefined; +} +export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) { + const theme = useTheme(); + + const anomalyScore = serviceAnomalyStats?.anomalyScore; + const anomalySeverity = getSeverity(anomalyScore); + const actualValue = serviceAnomalyStats?.actualValue; + const mlJobId = serviceAnomalyStats?.jobId; + const transactionType = + serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST; + const hasAnomalyDetectionScore = anomalyScore !== undefined; + + return ( + <> +
+ +

{ANOMALY_DETECTION_TITLE}

+
+   + + {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT}} +
+ {hasAnomalyDetectionScore && ( + + + + + + {ANOMALY_DETECTION_SCORE_METRIC} + + + +
+ {getDisplayedAnomalyScore(anomalyScore as number)} + {actualValue && ( +  ({asDuration(actualValue)}) + )} +
+
+
+
+ )} + {mlJobId && !hasAnomalyDetectionScore && ( + {ANOMALY_DETECTION_NO_DATA_TEXT} + )} + {mlJobId && ( + + + {ANOMALY_DETECTION_LINK} + + + )} + + ); +} + +function getDisplayedAnomalyScore(score: number) { + if (score > 0 && score < 1) { + return '< 1'; + } + return asInteger(score); +} + +const ANOMALY_DETECTION_TITLE = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', + { defaultMessage: 'Anomaly Detection' } +); + +const ANOMALY_DETECTION_TOOLTIP = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', + { + defaultMessage: + 'Service health indicators are powered by the anomaly detection feature in machine learning', + } +); + +const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric', + { defaultMessage: 'Score (max.)' } +); + +const ANOMALY_DETECTION_LINK = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverLink', + { defaultMessage: 'View anomalies' } +); + +const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', + { + defaultMessage: + 'Display service health indicators by enabling anomaly detection in APM settings.', + } +); + +const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData', + { + defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`, + } +); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 78779bdcc2052..c696a93773ceb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -15,7 +15,7 @@ import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; -import { popoverMinWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscapeOptions'; interface ContentsProps { isService: boolean; @@ -60,7 +60,7 @@ export function Contents({ @@ -68,16 +68,12 @@ export function Contents({ - {/* //TODO [APM ML] add service health stats here: - isService && ( - - - - - )*/} {isService ? ( - + ) : ( )} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index 2edd36f0d1380..ccf147ed1d90d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -12,40 +12,33 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) .add('example', () => ( - )) - .add('loading', () => ( - )) .add('some null values', () => ( )) .add('all null values', () => ( )); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index 718e43984d7f3..957678877a134 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -5,23 +5,38 @@ */ import React from 'react'; +import { + EuiLoadingSpinner, + EuiFlexGroup, + EuiHorizontalRule, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isNumber } from 'lodash'; import { ServiceNodeMetrics } from '../../../../../common/service_map'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; +import { AnomalyDetection } from './AnomalyDetection'; +import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; interface ServiceMetricFetcherProps { serviceName: string; + serviceAnomalyStats: ServiceAnomalyStats | undefined; } export function ServiceMetricFetcher({ serviceName, + serviceAnomalyStats, }: ServiceMetricFetcherProps) { const { urlParams: { start, end, environment }, } = useUrlParams(); - const { data = {} as ServiceNodeMetrics, status } = useFetcher( + const { + data = { transactionStats: {} } as ServiceNodeMetrics, + status, + } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ @@ -35,7 +50,62 @@ export function ServiceMetricFetcher({ preservePreviousData: false, } ); - const isLoading = status === 'loading'; - return ; + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + + if (isLoading) { + return ; + } + + const { + avgCpuUsage, + avgErrorsPerMinute, + avgMemoryUsage, + transactionStats: { avgRequestsPerMinute, avgTransactionDuration }, + } = data; + + const hasServiceData = [ + avgCpuUsage, + avgErrorsPerMinute, + avgMemoryUsage, + avgRequestsPerMinute, + avgTransactionDuration, + ].some((stat) => isNumber(stat)); + + if (environment && !hasServiceData) { + return ( + + {i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', { + defaultMessage: `No data for selected environment. Try switching to another environment.`, + })} + + ); + } + return ( + <> + {serviceAnomalyStats && ( + <> + + + + )} + + + ); +} + +function LoadingSpinner() { + return ( + + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index d66be9c61e42d..f82f434e7ded1 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; @@ -12,18 +11,6 @@ import styled from 'styled-components'; import { ServiceNodeMetrics } from '../../../../../common/service_map'; import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; -function LoadingSpinner() { - return ( - - - - ); -} - export const ItemRow = styled('tr')` line-height: 2; `; @@ -37,17 +24,13 @@ export const ItemDescription = styled('td')` text-align: right; `; -interface ServiceMetricListProps extends ServiceNodeMetrics { - isLoading: boolean; -} +type ServiceMetricListProps = ServiceNodeMetrics; export function ServiceMetricList({ - avgTransactionDuration, - avgRequestsPerMinute, avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, - isLoading, + transactionStats, }: ServiceMetricListProps) { const listItems = [ { @@ -57,8 +40,8 @@ export function ServiceMetricList({ defaultMessage: 'Trans. duration (avg.)', } ), - description: isNumber(avgTransactionDuration) - ? asDuration(avgTransactionDuration) + description: isNumber(transactionStats.avgTransactionDuration) + ? asDuration(transactionStats.avgTransactionDuration) : null, }, { @@ -68,8 +51,10 @@ export function ServiceMetricList({ defaultMessage: 'Req. per minute (avg.)', } ), - description: isNumber(avgRequestsPerMinute) - ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}` + description: isNumber(transactionStats.avgRequestsPerMinute) + ? `${transactionStats.avgRequestsPerMinute.toFixed(2)} ${tpmUnit( + 'request' + )}` : null, }, { @@ -100,9 +85,7 @@ export function ServiceMetricList({ }, ]; - return isLoading ? ( - - ) : ( + return ( {listItems.map( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 5a2a3d2a2644e..dfcfbee1806a4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -10,10 +10,11 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; -import { severity } from '../../../../common/ml_job_constants'; +import { severity, getSeverity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; +import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; -export const popoverMinWidth = 280; +export const popoverWidth = 280; export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { switch (nodeSeverity) { @@ -29,12 +30,19 @@ export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { } } +function getNodeSeverity(el: cytoscape.NodeSingular) { + const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( + 'serviceAnomalyStats' + ); + return getSeverity(serviceAnomalyStats?.anomalyScore); +} + function getBorderColorFn( theme: EuiTheme ): cytoscape.Css.MapperFunction { return (el: cytoscape.NodeSingular) => { - const hasAnomalyDetectionJob = el.data('ml_job_id') !== undefined; - const nodeSeverity = el.data('anomaly_severity'); + const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined; + const nodeSeverity = getNodeSeverity(el); if (hasAnomalyDetectionJob) { return ( getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade @@ -51,7 +59,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.NodeSingular, cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { - const nodeSeverity = el.data('anomaly_severity'); + const nodeSeverity = getNodeSeverity(el); if (nodeSeverity === severity.critical) { return 'double'; } else { @@ -60,7 +68,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< }; function getBorderWidth(el: cytoscape.NodeSingular) { - const nodeSeverity = el.data('anomaly_severity'); + const nodeSeverity = getNodeSeverity(el); if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { return 4; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 1e6015a9589b0..2f41b9fedd1d1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -25,7 +25,7 @@ export function AgentConfigurations() { (callApmApi) => callApmApi({ pathname: '/api/apm/settings/agent-configuration' }), [], - { preservePreviousData: false } + { preservePreviousData: false, showToastOnError: false } ); useTrackPageview({ app: 'apm', path: 'agent_configuration' }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 81655bc46c336..4ef3d78a7d303 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -10,9 +10,15 @@ import { i18n } from '@kbn/i18n'; import { EuiPanel } from '@elastic/eui'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/useFetcher'; import { LicensePrompt } from '../../../shared/LicensePrompt'; import { useLicense } from '../../../../hooks/useLicense'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +const DEFAULT_VALUE: APIReturnType<'/api/apm/settings/anomaly-detection'> = { + jobs: [], + hasLegacyJobs: false, +}; export const AnomalyDetection = () => { const license = useLicense(); @@ -20,17 +26,13 @@ export const AnomalyDetection = () => { const [viewAddEnvironments, setViewAddEnvironments] = useState(false); - const { refetch, data = [], status } = useFetcher( + const { refetch, data = DEFAULT_VALUE, status } = useFetcher( (callApmApi) => callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), [], - { preservePreviousData: false } + { preservePreviousData: false, showToastOnError: false } ); - const isLoading = - status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; - const hasFetchFailure = status === FETCH_STATUS.FAILURE; - if (!hasValidLicense) { return ( @@ -66,7 +68,7 @@ export const AnomalyDetection = () => { {viewAddEnvironments ? ( environment)} + currentEnvironments={data.jobs.map(({ environment }) => environment)} onCreateJobSuccess={() => { refetch(); setViewAddEnvironments(false); @@ -77,9 +79,9 @@ export const AnomalyDetection = () => { /> ) : ( { setViewAddEnvironments(true); }} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 30b4805011f03..674b4492c2c9c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,12 +16,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink'; import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { LegacyJobsCallout } from './legacy_jobs_callout'; const columns: Array> = [ { @@ -60,17 +62,22 @@ const columns: Array> = [ ]; interface Props { - isLoading: boolean; - hasFetchFailure: boolean; + status: FETCH_STATUS; onAddEnvironments: () => void; anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[]; + hasLegacyJobs: boolean; } export const JobsList = ({ - isLoading, - hasFetchFailure, + status, onAddEnvironments, anomalyDetectionJobsByEnv, + hasLegacyJobs, }: Props) => { + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + + const hasFetchFailure = status === FETCH_STATUS.FAILURE; + return ( @@ -131,6 +138,8 @@ export const JobsList = ({ items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv} /> + + {hasLegacyJobs && } ); }; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx new file mode 100644 index 0000000000000..54053097ab02e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx @@ -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 { EuiCallOut, EuiButton } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; + +export function LegacyJobsCallout() { + const { core } = useApmPluginContext(); + return ( + +

+ {i18n.translate( + 'xpack.apm.settings.anomaly_detection.legacy_jobs.body', + { + defaultMessage: + 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app', + } + )} +

+ + {i18n.translate( + 'xpack.apm.settings.anomaly_detection.legacy_jobs.button', + { defaultMessage: 'Review jobs' } + )} + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index b4cf3a65fea35..c832d3ded6175 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -18,8 +18,24 @@ describe('MLJobLink', () => { { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location ); - expect(href).toEqual( - `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"` + ); + }); + it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { + const href = await getRenderedHref( + () => ( + + ), + { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location + ); + + expect(href).toMatchInlineSnapshot( + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index 1e1f9ea5f23b7..f3c5b49287293 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -5,24 +5,35 @@ */ import React from 'react'; -import { MLLink } from './MLLink'; +import { EuiLink } from '@elastic/eui'; +import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; interface Props { jobId: string; external?: boolean; + serviceName?: string; + transactionType?: string; } -export const MLJobLink: React.FC = (props) => { - const query = { - ml: { jobIds: [props.jobId] }, - }; +export const MLJobLink: React.FC = ({ + jobId, + serviceName, + transactionType, + external, + children, +}) => { + const href = useTimeSeriesExplorerHref({ + jobId, + serviceName, + transactionType, + }); return ( - ); }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts new file mode 100644 index 0000000000000..625b9205b6ce0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.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 url from 'url'; +import querystring from 'querystring'; +import rison from 'rison-node'; +import { useLocation } from '../../../../hooks/useLocation'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { getTimepickerRisonData } from '../rison_helpers'; + +export function useTimeSeriesExplorerHref({ + jobId, + serviceName, + transactionType, +}: { + jobId: string; + serviceName?: string; + transactionType?: string; +}) { + const { core } = useApmPluginContext(); + const location = useLocation(); + + const search = querystring.stringify( + { + _g: rison.encode({ + ml: { jobIds: [jobId] }, + ...getTimepickerRisonData(location.search), + }), + ...(serviceName && transactionType + ? { + _a: rison.encode({ + mlTimeSeriesExplorer: { + entities: { + 'service.name': serviceName, + 'transaction.type': transactionType, + }, + }, + }), + } + : null), + }, + undefined, + undefined, + { + encodeURIComponent(str: string) { + return str; + }, + } + ); + + return url.format({ + pathname: core.http.basePath.prepend('/app/ml'), + hash: url.format({ pathname: '/timeseriesexplorer', search }), + }); +} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index f31ad83666a17..6e3a29d9f3dbc 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; -import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme'; import { ConfigSchema } from '.'; import { ObservabilityPluginSetup } from '../../observability/public'; import { @@ -83,7 +82,7 @@ export class ApmPlugin implements Plugin { plugins.observability.dashboard.register({ appName: 'apm', fetchData: async (params) => { - return fetchLandingPageData(params, { theme }); + return fetchLandingPageData(params); }, hasData, }); diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts index a14d827eeaec5..fd407a8bf72ad 100644 --- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -6,7 +6,6 @@ import { fetchLandingPageData, hasData } from './observability_dashboard'; import * as createCallApmApi from './createCallApmApi'; -import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme'; describe('Observability dashboard data', () => { const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); @@ -38,39 +37,31 @@ describe('Observability dashboard data', () => { ], }) ); - const response = await fetchLandingPageData( - { - startTime: '1', - endTime: '2', - bucketSize: '3', - }, - { theme } - ); + const response = await fetchLandingPageData({ + startTime: '1', + endTime: '2', + bucketSize: '3', + }); expect(response).toEqual({ title: 'APM', appLink: '/app/apm', stats: { services: { type: 'number', - label: 'Services', value: 10, }, transactions: { type: 'number', - label: 'Transactions', value: 2, - color: '#6092c0', }, }, series: { transactions: { - label: 'Transactions', coordinates: [ { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }, ], - color: '#6092c0', }, }, }); @@ -82,35 +73,27 @@ describe('Observability dashboard data', () => { transactionCoordinates: [], }) ); - const response = await fetchLandingPageData( - { - startTime: '1', - endTime: '2', - bucketSize: '3', - }, - { theme } - ); + const response = await fetchLandingPageData({ + startTime: '1', + endTime: '2', + bucketSize: '3', + }); expect(response).toEqual({ title: 'APM', appLink: '/app/apm', stats: { services: { type: 'number', - label: 'Services', value: 0, }, transactions: { type: 'number', - label: 'Transactions', value: 0, - color: '#6092c0', }, }, series: { transactions: { - label: 'Transactions', coordinates: [], - color: '#6092c0', }, }, }); @@ -122,35 +105,27 @@ describe('Observability dashboard data', () => { transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], }) ); - const response = await fetchLandingPageData( - { - startTime: '1', - endTime: '2', - bucketSize: '3', - }, - { theme } - ); + const response = await fetchLandingPageData({ + startTime: '1', + endTime: '2', + bucketSize: '3', + }); expect(response).toEqual({ title: 'APM', appLink: '/app/apm', stats: { services: { type: 'number', - label: 'Services', value: 0, }, transactions: { type: 'number', - label: 'Transactions', value: 0, - color: '#6092c0', }, }, series: { transactions: { - label: 'Transactions', coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], - color: '#6092c0', }, }, }); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts index 79ccf8dbd6f9b..409cec8b9ce10 100644 --- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -6,21 +6,17 @@ import { i18n } from '@kbn/i18n'; import { mean } from 'lodash'; -import { Theme } from '@kbn/ui-shared-deps/theme'; import { ApmFetchDataResponse, FetchDataParams, } from '../../../../observability/public'; import { callApmApi } from './createCallApmApi'; -interface Options { - theme: Theme; -} - -export const fetchLandingPageData = async ( - { startTime, endTime, bucketSize }: FetchDataParams, - { theme }: Options -): Promise => { +export const fetchLandingPageData = async ({ + startTime, + endTime, + bucketSize, +}: FetchDataParams): Promise => { const data = await callApmApi({ pathname: '/api/apm/observability_dashboard', params: { query: { start: startTime, end: endTime, bucketSize } }, @@ -36,34 +32,20 @@ export const fetchLandingPageData = async ( stats: { services: { type: 'number', - label: i18n.translate( - 'xpack.apm.observabilityDashboard.stats.services', - { defaultMessage: 'Services' } - ), value: serviceCount, }, transactions: { type: 'number', - label: i18n.translate( - 'xpack.apm.observabilityDashboard.stats.transactions', - { defaultMessage: 'Transactions' } - ), value: mean( transactionCoordinates .map(({ y }) => y) .filter((y) => y && isFinite(y)) ) || 0, - color: theme.euiColorVis1, }, }, series: { transactions: { - label: i18n.translate( - 'xpack.apm.observabilityDashboard.chart.transactions', - { defaultMessage: 'Transactions' } - ), - color: theme.euiColorVis1, coordinates: transactionCoordinates, }, }, diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts new file mode 100644 index 0000000000000..bfc4fcde09972 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.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 const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction'; +export const APM_ML_JOB_GROUP = 'apm'; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 406097805775d..e723393a24013 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -6,6 +6,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; +import { snakeCase } from 'lodash'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { @@ -14,9 +15,7 @@ import { PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; - -const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction'; -export const ML_GROUP_NAME_APM = 'apm'; +import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< typeof createAnomalyDetectionJobs @@ -78,13 +77,12 @@ async function createAnomalyDetectionJob({ environment: string; indexPatternName?: string | undefined; }) { - const convertedEnvironmentName = convertToMLIdentifier(environment); const randomToken = uuid().substr(-4); return ml.modules.setup({ moduleId: ML_MODULE_ID_APM_TRANSACTION, - prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`, - groups: [ML_GROUP_NAME_APM, convertedEnvironmentName], + prefix: `${APM_ML_JOB_GROUP}-${snakeCase(environment)}-${randomToken}-`, + groups: [APM_ML_JOB_GROUP], indexPatternName, query: { bool: { @@ -101,7 +99,11 @@ async function createAnomalyDetectionJob({ jobOverrides: [ { custom_settings: { - job_tags: { environment }, + job_tags: { + environment, + // identifies this as an APM ML job & facilitates future migrations + apm_ml_version: 2, + }, }, }, ], @@ -117,7 +119,3 @@ const ENVIRONMENT_NOT_DEFINED_FILTER = { }, }, }; - -export function convertToMLIdentifier(value: string) { - return value.replace(/\s+/g, '_').toLowerCase(); -} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 252c87e9263db..8fdebeb597eaf 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -5,56 +5,34 @@ */ import { Logger } from 'kibana/server'; -import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; -import { AnomalyDetectionJobByEnv } from '../../../typings/anomaly_detection'; -import { ML_GROUP_NAME_APM } from './create_anomaly_detection_jobs'; +import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group'; -export type AnomalyDetectionJobsAPIResponse = PromiseReturnType< - typeof getAnomalyDetectionJobs ->; -export async function getAnomalyDetectionJobs( - setup: Setup, - logger: Logger -): Promise { +export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const { ml } = setup; if (!ml) { return []; } - try { - const mlCapabilities = await ml.mlSystem.mlCapabilities(); - if ( - !( - mlCapabilities.mlFeatureEnabledInSpace && - mlCapabilities.isPlatinumOrTrialLicense - ) - ) { - logger.warn( - 'Anomaly detection integration is not availble for this user.' - ); - return []; - } - } catch (error) { - logger.warn('Unable to get ML capabilities.'); - logger.error(error); - return []; - } - try { - const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); - return jobs - .map((job) => { - const environment = job.custom_settings?.job_tags?.environment ?? ''; - return { - job_id: job.job_id, - environment, - }; - }) - .filter((job) => job.environment); - } catch (error) { - if (error.statusCode !== 404) { - logger.warn('Unable to get APM ML jobs.'); - logger.error(error); - } + + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if ( + !( + mlCapabilities.mlFeatureEnabledInSpace && + mlCapabilities.isPlatinumOrTrialLicense + ) + ) { + logger.warn('Anomaly detection integration is not availble for this user.'); return []; } + + const response = await getMlJobsWithAPMGroup(ml); + return response.jobs + .filter((job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2) + .map((job) => { + const environment = job.custom_settings?.job_tags?.environment ?? ''; + return { + job_id: job.job_id, + environment, + }; + }); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts new file mode 100644 index 0000000000000..5c0a3d17648aa --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.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 { Setup } from '../helpers/setup_request'; +import { APM_ML_JOB_GROUP } from './constants'; + +// returns ml jobs containing "apm" group +// workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned +export async function getMlJobsWithAPMGroup(ml: NonNullable) { + try { + return await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); + } catch (e) { + if (e.statusCode === 404) { + return { count: 0, jobs: [] }; + } + + throw e; + } +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts new file mode 100644 index 0000000000000..bf502607fcc1d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.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 { Setup } from '../helpers/setup_request'; +import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group'; + +// Determine whether there are any legacy ml jobs. +// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction +export async function hasLegacyJobs(setup: Setup) { + const { ml } = setup; + + if (!ml) { + return false; + } + + const response = await getMlJobsWithAPMGroup(ml); + return response.jobs.some( + (job) => + job.job_id.endsWith('high_mean_response_time') && + job.custom_settings?.created_by === 'ml-module-apm-transaction' + ); +} diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts index c648cf4cc116a..e3161b49b315d 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts @@ -65,4 +65,40 @@ describe('data telemetry collection tasks', () => { }); }); }); + + describe('integrations', () => { + const integrationsTask = tasks.find((task) => task.name === 'integrations'); + + it('returns the count of ML jobs', async () => { + const transportRequest = jest + .fn() + .mockResolvedValueOnce({ body: { count: 1 } }); + + expect( + await integrationsTask?.executor({ indices, transportRequest } as any) + ).toEqual({ + integrations: { + ml: { + all_jobs_count: 1, + }, + }, + }); + }); + + describe('with no data', () => { + it('returns a count of 0', async () => { + const transportRequest = jest.fn().mockResolvedValueOnce({}); + + expect( + await integrationsTask?.executor({ indices, transportRequest } as any) + ).toEqual({ + integrations: { + ml: { + all_jobs_count: 0, + }, + }, + }); + }); + }); + }); }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index f27af9a2cc516..4bbaaf3e86e78 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -4,31 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import { flatten, merge, sortBy, sum } from 'lodash'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { TelemetryTask } from '.'; import { AGENT_NAMES } from '../../../../common/agent_name'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { - PROCESSOR_EVENT, - SERVICE_NAME, AGENT_NAME, AGENT_VERSION, + CLOUD_AVAILABILITY_ZONE, + CLOUD_PROVIDER, + CLOUD_REGION, ERROR_GROUP_ID, - TRANSACTION_NAME, PARENT_ID, + PROCESSOR_EVENT, SERVICE_FRAMEWORK_NAME, SERVICE_FRAMEWORK_VERSION, SERVICE_LANGUAGE_NAME, SERVICE_LANGUAGE_VERSION, + SERVICE_NAME, SERVICE_RUNTIME_NAME, SERVICE_RUNTIME_VERSION, + TRANSACTION_NAME, USER_AGENT_ORIGINAL, - CLOUD_AVAILABILITY_ZONE, - CLOUD_PROVIDER, - CLOUD_REGION, } from '../../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../../typings/es_schemas/ui/span'; import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; -import { TelemetryTask } from '.'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { Span } from '../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { APMTelemetry } from '../types'; const TIME_RANGES = ['1d', 'all'] as const; @@ -465,17 +465,17 @@ export const tasks: TelemetryTask[] = [ { name: 'integrations', executor: async ({ transportRequest }) => { - const apmJobs = ['*-high_mean_response_time']; + const apmJobs = ['apm-*', '*-high_mean_response_time']; const response = (await transportRequest({ method: 'get', path: `/_ml/anomaly_detectors/${apmJobs.join(',')}`, - })) as { data?: { count: number } }; + })) as { body?: { count: number } }; return { integrations: { ml: { - all_jobs_count: response.data?.count ?? 0, + all_jobs_count: response.body?.count ?? 0, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts new file mode 100644 index 0000000000000..3e5ef5eb37b02 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger } from 'kibana/server'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { PromiseReturnType } from '../../../typings/common'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; +import { APM_ML_JOB_GROUP } from '../anomaly_detection/constants'; + +export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; + +export type ServiceAnomaliesResponse = PromiseReturnType< + typeof getServiceAnomalies +>; + +export async function getServiceAnomalies({ + setup, + logger, + environment, +}: { + setup: Setup & SetupTimeRange; + logger: Logger; + environment?: string; +}) { + const { ml, start, end } = setup; + + if (!ml) { + logger.warn('Anomaly detection plugin is not available.'); + return DEFAULT_ANOMALIES; + } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { + logger.warn('Anomaly detection feature is not enabled for the space.'); + return DEFAULT_ANOMALIES; + } + if (!mlCapabilities.isPlatinumOrTrialLicense) { + logger.warn( + 'Unable to create anomaly detection jobs due to insufficient license.' + ); + return DEFAULT_ANOMALIES; + } + + let mlJobIds: string[] = []; + try { + mlJobIds = await getMLJobIds(ml, environment); + } catch (error) { + logger.error(error); + return DEFAULT_ANOMALIES; + } + + const params = { + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { result_type: 'record' } }, + { terms: { job_id: mlJobIds } }, + { + range: { + timestamp: { gte: start, lte: end, format: 'epoch_millis' }, + }, + }, + { + terms: { + // Only retrieving anomalies for transaction types "request" and "page-load" + by_field_value: [TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD], + }, + }, + ], + }, + }, + aggs: { + services: { + terms: { field: 'partition_field_value' }, + aggs: { + top_score: { + top_hits: { + sort: { record_score: 'desc' }, + _source: { includes: ['actual', 'job_id', 'by_field_value'] }, + size: 1, + }, + }, + }, + }, + }, + }, + }; + const response = await ml.mlSystem.mlAnomalySearch(params); + return { + mlJobIds, + serviceAnomalies: transformResponseToServiceAnomalies( + response as ServiceAnomaliesAggResponse + ), + }; +} + +interface ServiceAnomaliesAggResponse { + aggregations: { + services: { + buckets: Array<{ + key: string; + top_score: { + hits: { + hits: Array<{ + sort: [number]; + _source: { + actual: [number]; + job_id: string; + by_field_value: string; + }; + }>; + }; + }; + }>; + }; + }; +} + +function transformResponseToServiceAnomalies( + response: ServiceAnomaliesAggResponse +): Record { + const serviceAnomaliesMap = response.aggregations.services.buckets.reduce( + (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { + return { + ...statsByServiceName, + [serviceName]: { + transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value, + anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0], + actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0], + jobId: topScoreAgg.hits.hits[0]?._source?.job_id, + }, + }; + }, + {} + ); + return serviceAnomaliesMap; +} + +export async function getMLJobIds( + ml: Required['ml'], + environment?: string +) { + const response = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); + // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` + // and checking that it is compatable. + const mlJobs = response.jobs.filter( + (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 + ); + if (environment) { + const matchingMLJob = mlJobs.find( + (job) => job.custom_settings?.job_tags?.environment === environment + ); + if (!matchingMLJob) { + throw new Error(`ML job Not Found for environment "${environment}".`); + } + return [matchingMLJob.job_id]; + } + return mlJobs.map((job) => job.job_id); +} 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 4d488cd1a5509..ea2bb14efdfc7 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { chunk } from 'lodash'; +import { Logger } from 'kibana/server'; import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -16,11 +17,17 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; +import { + getServiceAnomalies, + ServiceAnomaliesResponse, + DEFAULT_ANOMALIES, +} from './get_service_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; serviceName?: string; environment?: string; + logger: Logger; } async function getConnectionData({ @@ -132,13 +139,23 @@ export type ServicesResponse = PromiseReturnType; export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { - const [connectionData, servicesData] = await Promise.all([ + const { logger } = options; + const anomaliesPromise: Promise = getServiceAnomalies( + options + ).catch((error) => { + logger.warn(`Unable to retrieve anomalies for service maps.`); + logger.error(error); + return DEFAULT_ANOMALIES; + }); + const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), getServicesData(options), + anomaliesPromise, ]); return transformServiceMapResponses({ ...connectionData, services: servicesData, + anomalies, }); } 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 e521efa687388..be92bfe5a0099 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 @@ -12,11 +12,17 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_DURATION, + TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, } from '../../../common/elasticsearch_fieldnames'; import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; +import { + TRANSACTION_REQUEST, + TRANSACTION_PAGE_LOAD, +} from '../../../common/transaction_types'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; interface Options { setup: Setup & SetupTimeRange; @@ -37,12 +43,23 @@ export async function getServiceMapServiceNodeInfo({ }: Options & { serviceName: string; environment?: string }) { const { start, end } = setup; + const environmentNotDefinedFilter = { + bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] }, + }; + const filter: ESFilter[] = [ { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, - ...(environment ? [{ term: { [SERVICE_ENVIRONMENT]: environment } }] : []), ]; + if (environment) { + filter.push( + environment === ENVIRONMENT_NOT_DEFINED + ? environmentNotDefinedFilter + : { term: { [SERVICE_ENVIRONMENT]: environment } } + ); + } + const minutes = Math.abs((end - start) / (1000 * 60)); const taskParams = { @@ -53,19 +70,19 @@ export async function getServiceMapServiceNodeInfo({ const [ errorMetrics, - transactionMetrics, + transactionStats, cpuMetrics, memoryMetrics, ] = await Promise.all([ getErrorMetrics(taskParams), - getTransactionMetrics(taskParams), + getTransactionStats(taskParams), getCpuMetrics(taskParams), getMemoryMetrics(taskParams), ]); return { ...errorMetrics, - ...transactionMetrics, + transactionStats, ...cpuMetrics, ...memoryMetrics, }; @@ -99,7 +116,7 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) { }; } -async function getTransactionMetrics({ +async function getTransactionStats({ setup, filter, minutes, @@ -109,17 +126,28 @@ async function getTransactionMetrics({ }> { const { indices, client } = setup; - const response = await client.search({ + const params = { index: indices['apm_oss.transactionIndices'], body: { - size: 1, + size: 0, query: { bool: { - filter: filter.concat({ - term: { - [PROCESSOR_EVENT]: 'transaction', + filter: [ + ...filter, + { + term: { + [PROCESSOR_EVENT]: 'transaction', + }, }, - }), + { + terms: { + [TRANSACTION_TYPE]: [ + TRANSACTION_REQUEST, + TRANSACTION_PAGE_LOAD, + ], + }, + }, + ], }, }, track_total_hits: true, @@ -131,14 +159,12 @@ async function getTransactionMetrics({ }, }, }, - }); - + }; + const response = await client.search(params); + const docCount = response.hits.total.value; return { avgTransactionDuration: response.aggregations?.duration.value ?? null, - avgRequestsPerMinute: - response.hits.total.value > 0 - ? response.hits.total.value / minutes - : null, + avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 1e26634bdf0f1..7e4bcfdda7382 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -35,6 +35,18 @@ const javaService = { [AGENT_NAME]: 'java', }; +const anomalies = { + mlJobIds: ['apm-test-1234-ml-module-name'], + serviceAnomalies: { + 'opbeans-test': { + transactionType: 'request', + actualValue: 10000, + anomalyScore: 50, + jobId: 'apm-test-1234-ml-module-name', + }, + }, +}; + describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { @@ -51,6 +63,7 @@ describe('transformServiceMapResponses', () => { destination: nodejsExternal, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); @@ -89,6 +102,7 @@ describe('transformServiceMapResponses', () => { }, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); @@ -126,6 +140,7 @@ describe('transformServiceMapResponses', () => { }, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); @@ -150,6 +165,7 @@ describe('transformServiceMapResponses', () => { destination: nodejsService, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 2e394f44b25b1..7f5e34f68f922 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -18,6 +18,7 @@ import { ExternalConnectionNode, } from '../../../common/service_map'; import { ConnectionsResponse, ServicesResponse } from './get_service_map'; +import { ServiceAnomaliesResponse } from './get_service_anomalies'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -63,10 +64,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { export type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse; + anomalies: ServiceAnomaliesResponse; }; export function transformServiceMapResponses(response: ServiceMapResponse) { - const { discoveredServices, services, connections } = response; + const { discoveredServices, services, connections, anomalies } = response; const allNodes = getAllNodes(services, connections); const serviceNodes = getServiceNodes(allNodes); @@ -100,21 +102,23 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { serviceName = node[SERVICE_NAME]; } - const matchedServiceNodes = serviceNodes.filter( - (serviceNode) => serviceNode[SERVICE_NAME] === serviceName - ); + const matchedServiceNodes = serviceNodes + .filter((serviceNode) => serviceNode[SERVICE_NAME] === serviceName) + .map((serviceNode) => pickBy(serviceNode, identity)); + const mergedServiceNode = Object.assign({}, ...matchedServiceNodes); + + const serviceAnomalyStats = serviceName + ? anomalies.serviceAnomalies[serviceName] + : null; if (matchedServiceNodes.length) { return { ...map, - [node.id]: Object.assign( - { - id: matchedServiceNodes[0][SERVICE_NAME], - }, - ...matchedServiceNodes.map((serviceNode) => - pickBy(serviceNode, identity) - ) - ), + [node.id]: { + id: matchedServiceNodes[0][SERVICE_NAME], + ...mergedServiceNode, + ...(serviceAnomalyStats ? { serviceAnomalyStats } : null), + }, }; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index a3e2f708b0b22..50123131a42e7 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -37,11 +37,12 @@ export const serviceMapRoute = createRoute(() => ({ } context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); + const logger = context.logger; const setup = await setupRequest(context, request); const { query: { serviceName, environment }, } = context.params; - return getServiceMap({ setup, serviceName, environment }); + return getServiceMap({ setup, serviceName, environment, logger }); }, })); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 67eca0da946d0..7009470e1ff17 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -10,6 +10,7 @@ import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; +import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -17,7 +18,11 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ path: '/api/apm/settings/anomaly-detection', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await getAnomalyDetectionJobs(setup, context.logger); + const jobs = await getAnomalyDetectionJobs(setup, context.logger); + return { + jobs, + hasLegacyJobs: await hasLegacyJobs(setup), + }; }, })); diff --git a/x-pack/plugins/canvas/.storybook/storyshots.test.js b/x-pack/plugins/canvas/.storybook/storyshots.test.js index e3217ad4dbe58..b9fe0914b3698 100644 --- a/x-pack/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/.storybook/storyshots.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import fs from 'fs'; import path from 'path'; import moment from 'moment'; import 'moment-timezone'; @@ -77,12 +76,6 @@ import { RenderedElement } from '../shareable_runtime/components/rendered_elemen jest.mock('../shareable_runtime/components/rendered_element'); RenderedElement.mockImplementation(() => 'RenderedElement'); -// Some of the code requires that this directory exists, but the tests don't actually require any css to be present -const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); -if (!fs.existsSync(cssDir)) { - fs.mkdirSync(cssDir, { recursive: true }); -} - addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 225432375dc75..e5037a6477aca 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -5,6 +5,8 @@ */ export const POLICY_NAME = 'my_policy'; +export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; +export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy'; export const DELETE_PHASE_POLICY = { version: 1, @@ -26,7 +28,7 @@ export const DELETE_PHASE_POLICY = { min_age: '0ms', actions: { wait_for_snapshot: { - policy: 'my_snapshot_policy', + policy: SNAPSHOT_POLICY_NAME, }, delete: { delete_searchable_snapshot: true, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index d6c955e0c0813..cba496ee0f212 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; @@ -14,6 +15,25 @@ import { TestSubjects } from '../helpers'; import { EditPolicy } from '../../../public/application/sections/edit_policy'; import { indexLifecycleManagementStore } from '../../../public/application/store'; +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); + const testBedConfig: TestBedConfig = { store: () => indexLifecycleManagementStore(), memoryRouter: { @@ -34,9 +54,11 @@ export interface EditPolicyTestBed extends TestBed { export const setup = async (): Promise => { const testBed = await initTestBed(); - const setWaitForSnapshotPolicy = (snapshotPolicyName: string) => { - const { component, form } = testBed; - form.setInputValue('waitForSnapshotField', snapshotPolicyName, true); + const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => { + const { component } = testBed; + act(() => { + testBed.find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); + }); component.update(); }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 8753f01376d42..06829e6ef6f1e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -7,11 +7,10 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../helpers/setup_environment'; - import { EditPolicyTestBed, setup } from './edit_policy.helpers'; -import { DELETE_PHASE_POLICY } from './constants'; import { API_BASE_PATH } from '../../../common/constants'; +import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME } from './constants'; window.scrollTo = jest.fn(); @@ -25,6 +24,10 @@ describe('', () => { describe('delete phase', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([ + SNAPSHOT_POLICY_NAME, + NEW_SNAPSHOT_POLICY_NAME, + ]); await act(async () => { testBed = await setup(); @@ -35,16 +38,18 @@ describe('', () => { }); test('wait for snapshot policy field should correctly display snapshot policy name', () => { - expect(testBed.find('waitForSnapshotField').props().value).toEqual( - DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy - ); + expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ + { + label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + value: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + }, + ]); }); test('wait for snapshot field should correctly update snapshot policy name', async () => { const { actions } = testBed; - const newPolicyName = 'my_new_snapshot_policy'; - actions.setWaitForSnapshotPolicy(newPolicyName); + await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME); await actions.savePolicy(); const expected = { @@ -56,7 +61,7 @@ describe('', () => { actions: { ...DELETE_PHASE_POLICY.policy.phases.delete.actions, wait_for_snapshot: { - policy: newPolicyName, + policy: NEW_SNAPSHOT_POLICY_NAME, }, }, }, @@ -69,6 +74,15 @@ describe('', () => { expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); + test('wait for snapshot field should display a callout when the input is not an existing policy', async () => { + const { actions } = testBed; + + await actions.setWaitForSnapshotPolicy('my_custom_policy'); + expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); + expect(testBed.find('customPolicyCallout').exists()).toBeTruthy(); + }); + test('wait for snapshot field should delete action if field is empty', async () => { const { actions } = testBed; @@ -92,5 +106,31 @@ describe('', () => { const latestRequest = server.requests[server.requests.length - 1]; expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); + + test('wait for snapshot field should display a callout when there are no snapshot policies', async () => { + // need to call setup on testBed again for it to use a newly defined snapshot policies response + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); + expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy(); + }); + + test('wait for snapshot field should display a callout when there is an error loading snapshot policies', async () => { + // need to call setup on testBed again for it to use a newly defined snapshot policies response + httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' }); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); + expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index f41742fc104ff..04f58f93939ca 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SinonFakeServer, fakeServer } from 'sinon'; +import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; export const init = () => { @@ -27,7 +27,19 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadSnapshotPolicies = (response: any = [], error?: { status: number; body: any }) => { + const status = error ? error.status : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/snapshot_policies`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadPolicies, + setLoadSnapshotPolicies, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 3cff2e3ab050f..7b227f822fa97 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export type TestSubjects = 'waitForSnapshotField' | 'savePolicyButton'; +export type TestSubjects = + | 'snapshotPolicyCombobox' + | 'savePolicyButton' + | 'customPolicyCallout' + | 'noPoliciesCallout' + | 'policiesErrorCallout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js index 299bf28778ab4..34d1c0f8de216 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js @@ -7,17 +7,12 @@ import React, { PureComponent, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescribedFormGroup, - EuiSwitch, - EuiFieldText, - EuiTextColor, - EuiFormRow, -} from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../../constants'; import { ActiveBadge, LearnMoreLink, OptionalLabel, PhaseErrorMessage } from '../../../components'; import { MinAgeInput } from '../min_age_input'; +import { SnapshotPolicies } from '../snapshot_policies'; export class DeletePhase extends PureComponent { static propTypes = { @@ -125,10 +120,9 @@ export class DeletePhase extends PureComponent { } > - setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, e.target.value)} + onChange={(value) => setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts new file mode 100644 index 0000000000000..f33ce81eb6157 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SnapshotPolicies } from './snapshot_policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx new file mode 100644 index 0000000000000..76eae0f906d0c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiComboBox, + EuiComboBoxOptionOption, + EuiSpacer, +} from '@elastic/eui'; + +import { useLoadSnapshotPolicies } from '../../../../services/api'; + +interface Props { + value: string; + onChange: (value: string) => void; +} +export const SnapshotPolicies: React.FunctionComponent = ({ value, onChange }) => { + const { error, isLoading, data, sendRequest } = useLoadSnapshotPolicies(); + + const policies = data.map((name: string) => ({ + label: name, + value: name, + })); + + const onComboChange = (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + onChange(options[0].label); + } else { + onChange(''); + } + }; + + const onCreateOption = (newValue: string) => { + onChange(newValue); + }; + + let calloutContent; + if (error) { + calloutContent = ( + + + + + + + + } + > + + + + ); + } else if (data.length === 0) { + calloutContent = ( + + + + } + > + + + + ); + } else if (value && !data.includes(value)) { + calloutContent = ( + + + + } + > + + + + ); + } + + return ( + + + {calloutContent} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index dad259681eb7a..500ab44d96694 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js @@ -254,7 +254,7 @@ export class PolicyTable extends Component { icon: 'list', onClick: () => { this.props.navigateToApp('management', { - path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`)}`, + path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`, }); }, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts similarity index 56% rename from x-pack/plugins/index_lifecycle_management/public/application/services/api.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 6b46d6e6ea735..065fb3bcebca7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { METRIC_TYPE } from '@kbn/analytics'; +import { trackUiMetric } from './ui_metric'; + import { UIM_POLICY_DELETE, UIM_POLICY_ATTACH_INDEX, @@ -12,14 +15,13 @@ import { UIM_INDEX_RETRY_STEP, } from '../constants'; -import { trackUiMetric } from './ui_metric'; -import { sendGet, sendPost, sendDelete } from './http'; +import { sendGet, sendPost, sendDelete, useRequest } from './http'; export async function loadNodes() { return await sendGet(`nodes/list`); } -export async function loadNodeDetails(selectedNodeAttrs) { +export async function loadNodeDetails(selectedNodeAttrs: string) { return await sendGet(`nodes/${selectedNodeAttrs}/details`); } @@ -27,45 +29,53 @@ export async function loadIndexTemplates() { return await sendGet(`templates`); } -export async function loadPolicies(withIndices) { +export async function loadPolicies(withIndices: boolean) { return await sendGet('policies', { withIndices }); } -export async function savePolicy(policy) { +export async function savePolicy(policy: any) { return await sendPost(`policies`, policy); } -export async function deletePolicy(policyName) { +export async function deletePolicy(policyName: string) { const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DELETE); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DELETE); return response; } -export const retryLifecycleForIndex = async (indexNames) => { +export const retryLifecycleForIndex = async (indexNames: string[]) => { const response = await sendPost(`index/retry`, { indexNames }); // Only track successful actions. - trackUiMetric('count', UIM_INDEX_RETRY_STEP); + trackUiMetric(METRIC_TYPE.COUNT, UIM_INDEX_RETRY_STEP); return response; }; -export const removeLifecycleForIndex = async (indexNames) => { +export const removeLifecycleForIndex = async (indexNames: string[]) => { const response = await sendPost(`index/remove`, { indexNames }); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DETACH_INDEX); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DETACH_INDEX); return response; }; -export const addLifecyclePolicyToIndex = async (body) => { +export const addLifecyclePolicyToIndex = async (body: any) => { const response = await sendPost(`index/add`, body); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX); return response; }; -export const addLifecyclePolicyToTemplate = async (body) => { +export const addLifecyclePolicyToTemplate = async (body: any) => { const response = await sendPost(`template`, body); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX_TEMPLATE); return response; }; + +export const useLoadSnapshotPolicies = () => { + return useRequest({ + path: `snapshot_policies`, + method: 'get', + initialData: [], + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts index 47e96ea28bb8c..c54ee15fd69bf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + UseRequestConfig, + useRequest as _useRequest, + Error, +} from '../../../../../../src/plugins/es_ui_shared/public'; + let _httpClient: any; export function init(httpClient: any): void { @@ -24,10 +30,14 @@ export function sendPost(path: string, payload: any): any { return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload) }); } -export function sendGet(path: string, query: any): any { +export function sendGet(path: string, query?: any): any { return _httpClient.get(getFullPath(path), { query }); } export function sendDelete(path: string): any { return _httpClient.delete(getFullPath(path)); } + +export const useRequest = (config: UseRequestConfig) => { + return _useRequest(_httpClient, { ...config, path: getFullPath(config.path) }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts new file mode 100644 index 0000000000000..19fbc45010ea2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerFetchRoute } from './register_fetch_route'; + +export function registerSnapshotPoliciesRoutes(dependencies: RouteDependencies) { + registerFetchRoute(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts new file mode 100644 index 0000000000000..7a52648e29ee8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LegacyAPICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function fetchSnapshotPolicies(callAsCurrentUser: LegacyAPICaller): Promise { + const params = { + method: 'GET', + path: '/_slm/policy', + }; + + return await callAsCurrentUser('transport.request', params); +} + +export function registerFetchRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/snapshot_policies'), validate: false }, + license.guardApiRoute(async (context, request, response) => { + try { + const policiesByName = await fetchSnapshotPolicies( + context.core.elasticsearch.legacy.client.callAsCurrentUser + ); + return response.ok({ body: Object.keys(policiesByName) }); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts index 35996721854c6..f7390debbe177 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts @@ -10,10 +10,12 @@ import { registerIndexRoutes } from './api/index'; import { registerNodesRoutes } from './api/nodes'; import { registerPoliciesRoutes } from './api/policies'; import { registerTemplatesRoutes } from './api/templates'; +import { registerSnapshotPoliciesRoutes } from './api/snapshot_policies'; export function registerApiRoutes(dependencies: RouteDependencies) { registerIndexRoutes(dependencies); registerNodesRoutes(dependencies); registerPoliciesRoutes(dependencies); registerTemplatesRoutes(dependencies); + registerSnapshotPoliciesRoutes(dependencies); } diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index d85db94d4a970..ad445f75f047c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -34,7 +34,11 @@ export const services = { services.uiMetricService.setup({ reportUiStats() {} } as any); setExtensionsService(services.extensionsService); setUiMetricService(services.uiMetricService); -const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any; +const appDependencies = { + services, + core: { getUrlForApp: () => {} }, + plugins: {}, +} as any; export const setupEnvironment = () => { // Mock initialization of services diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index ecea230ecab85..9397ce21ba827 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -166,7 +166,7 @@ export const setup = async (overridingDependencies: any = {}): Promise ({ name, - timeStampField: { name: '@timestamp', mapping: { type: 'date' } }, + timeStampField: { name: '@timestamp' }, indices: [ { name: 'indexName', diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 0047e4c0294cb..a397419053351 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -95,9 +95,9 @@ const createActions = (testBed: TestBed) => { find('closeDetailsButton').simulate('click'); }; - const toggleViewItem = (view: 'composable' | 'system') => { + const toggleViewItem = (view: 'managed' | 'cloudManaged' | 'system') => { const { find, component } = testBed; - const views = ['composable', 'system']; + const views = ['managed', 'cloudManaged', 'system']; // First open the pop over act(() => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index 1ec29f1c5b894..276101486aa61 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -63,7 +63,6 @@ describe('Index Templates tab', () => { }, }, }); - (template1 as any).hasSettings = true; const template2 = fixtures.getTemplate({ name: `b${getRandomString()}`, @@ -73,6 +72,7 @@ describe('Index Templates tab', () => { const template3 = fixtures.getTemplate({ name: `.c${getRandomString()}`, // mock system template indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'], + type: 'system', }); const template4 = fixtures.getTemplate({ @@ -101,6 +101,7 @@ describe('Index Templates tab', () => { name: `.c${getRandomString()}`, // mock system template indexPatterns: ['template6Pattern1*', 'template6Pattern2', 'template6Pattern3'], isLegacy: true, + type: 'system', }); const templates = [template1, template2, template3]; @@ -124,44 +125,49 @@ describe('Index Templates tab', () => { // Test composable table content tableCellsValues.forEach((row, i) => { const indexTemplate = templates[i]; - const { name, indexPatterns, priority, ilmPolicy, composedOf, template } = indexTemplate; + const { name, indexPatterns, ilmPolicy, composedOf, template } = indexTemplate; const hasContent = !!template.settings || !!template.mappings || !!template.aliases; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; const composedOfString = composedOf ? composedOf.join(',') : ''; - const priorityFormatted = priority ? priority.toString() : ''; - - expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ - '', // Checkbox to select row - name, - indexPatterns.join(', '), - ilmPolicyName, - composedOfString, - priorityFormatted, - hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges - '', // Column of actions - ]); + + try { + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', // Checkbox to select row + name, + indexPatterns.join(', '), + ilmPolicyName, + composedOfString, + hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges + '', // Column of actions + ]); + } catch (e) { + console.error(`Error in index template at row ${i}`); // eslint-disable-line no-console + throw e; + } }); // Test legacy table content legacyTableCellsValues.forEach((row, i) => { - const template = legacyTemplates[i]; - const { name, indexPatterns, order, ilmPolicy } = template; + const legacyIndexTemplate = legacyTemplates[i]; + const { name, indexPatterns, ilmPolicy, template } = legacyIndexTemplate; + const hasContent = !!template.settings || !!template.mappings || !!template.aliases; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; - const orderFormatted = order ? order.toString() : order; - - expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ - '', - name, - indexPatterns.join(', '), - ilmPolicyName, - orderFormatted, - '', - '', - '', - '', - ]); + + try { + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', + name, + indexPatterns.join(', '), + ilmPolicyName, + hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges + '', // Column of actions + ]); + } catch (e) { + console.error(`Error in legacy template at row ${i}`); // eslint-disable-line no-console + throw e; + } }); }); @@ -211,7 +217,7 @@ describe('Index Templates tab', () => { await actions.clickTemplateAt(0); expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(templates[0].name); + expect(find('templateDetails.title').text().trim()).toBe(templates[0].name); // Close flyout await act(async () => { @@ -223,7 +229,7 @@ describe('Index Templates tab', () => { expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); + expect(find('templateDetails.title').text().trim()).toBe(legacyTemplates[0].name); }); describe('table row actions', () => { @@ -460,7 +466,7 @@ describe('Index Templates tab', () => { const { find } = testBed; const [{ name }] = templates; - expect(find('templateDetails.title').text()).toEqual(name); + expect(find('templateDetails.title').text().trim()).toEqual(name); }); it('should have a close button and be able to close flyout', async () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 69d7a13edfcfb..76b6c34f999d5 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -368,8 +368,8 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { + type: 'default', isLegacy: false, - isManaged: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 9f0e81454f0af..de66013241236 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -213,7 +213,7 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { - isManaged: false, + type: 'default', isLegacy: templateToEdit._kbnMeta.isLegacy, }, }; diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 5c55860bda81b..069d6ac29fbca 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -8,18 +8,28 @@ import { LegacyTemplateSerialized, TemplateSerialized, TemplateListItem, + TemplateType, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized { - const { version, priority, indexPatterns, template, composedOf, _meta } = templateDeserialized; + const { + version, + priority, + indexPatterns, + template, + composedOf, + dataStream, + _meta, + } = templateDeserialized; return { version, priority, template, index_patterns: indexPatterns, + data_stream: dataStream, composed_of: composedOf, _meta, }; @@ -41,6 +51,15 @@ export function deserializeTemplate( } = templateEs; const { settings } = template; + let type: TemplateType = 'default'; + if (Boolean(cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix))) { + type = 'cloudManaged'; + } else if (name.startsWith('.')) { + type = 'system'; + } else if (Boolean(_meta?.managed === true)) { + type = 'managed'; + } + const deserializedTemplate: TemplateDeserialized = { name, version, @@ -52,10 +71,7 @@ export function deserializeTemplate( dataStream, _meta, _kbnMeta: { - isManaged: Boolean(_meta?.managed === true), - isCloudManaged: Boolean( - cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix) - ), + type, hasDatastream: Boolean(dataStream), }, }; diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts index 5a7db8ef50ab4..1dc6f4a486a2c 100644 --- a/x-pack/plugins/index_management/common/lib/utils.ts +++ b/x-pack/plugins/index_management/common/lib/utils.ts @@ -23,5 +23,5 @@ export const getTemplateParameter = ( ) => { return isLegacyTemplate(template) ? (template as LegacyTemplateSerialized)[setting] - : (template as TemplateSerialized).template[setting]; + : (template as TemplateSerialized).template?.[setting]; }; diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts index 772ed43459bcf..d1936c4426b49 100644 --- a/x-pack/plugins/index_management/common/types/data_streams.ts +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -6,9 +6,6 @@ interface TimestampFieldFromEs { name: string; - mapping: { - type: string; - }; } type TimestampField = TimestampFieldFromEs; diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index fdcac40ca596f..32e254e490b2a 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -38,23 +38,24 @@ export interface TemplateDeserialized { aliases?: Aliases; mappings?: Mappings; }; - composedOf?: string[]; // Used on composable index template + composedOf?: string[]; // Composable template only version?: number; - priority?: number; - order?: number; // Used on legacy index template + priority?: number; // Composable template only + order?: number; // Legacy template only ilmPolicy?: { name: string; }; - _meta?: { [key: string]: any }; - dataStream?: { timestamp_field: string }; + _meta?: { [key: string]: any }; // Composable template only + dataStream?: { timestamp_field: string }; // Composable template only _kbnMeta: { - isManaged: boolean; - isCloudManaged: boolean; + type: TemplateType; hasDatastream: boolean; isLegacy?: boolean; }; } +export type TemplateType = 'default' | 'managed' | 'cloudManaged' | 'system'; + export interface TemplateFromEs { name: string; index_template: TemplateSerialized; @@ -78,8 +79,7 @@ export interface TemplateListItem { name: string; }; _kbnMeta: { - isManaged: boolean; - isCloudManaged: boolean; + type: TemplateType; hasDatastream: boolean; isLegacy?: boolean; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx index 6c8da4684f019..75eb419d56a5c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx @@ -177,8 +177,6 @@ describe('', () => { template: { settings: SETTINGS, mappings: { - _source: {}, - _meta: {}, properties: { [BOOLEAN_MAPPING_FIELD.name]: { type: BOOLEAN_MAPPING_FIELD.type, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index f237605756d5c..115fdf032da8f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -109,11 +109,6 @@ describe('', () => { ...COMPONENT_TEMPLATE_TO_EDIT, template: { ...COMPONENT_TEMPLATE_TO_EDIT.template, - mappings: { - _meta: {}, - _source: {}, - properties: {}, - }, }, }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx index 64c7cd400ba0d..ea5632ac86192 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx @@ -26,8 +26,15 @@ interface Filters { [key: string]: { name: string; checked: 'on' | 'off' }; } +/** + * Copied from https://stackoverflow.com/a/9310752 + */ +function escapeRegExp(text: string) { + return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +} + function fuzzyMatch(searchValue: string, text: string) { - const pattern = `.*${searchValue.split('').join('.*')}.*`; + const pattern = `.*${searchValue.split('').map(escapeRegExp).join('.*')}.*`; const regex = new RegExp(pattern); return regex.test(text); } @@ -48,7 +55,7 @@ const i18nTexts = { searchBoxPlaceholder: i18n.translate( 'xpack.idxMgmt.componentTemplatesSelector.searchBox.placeholder', { - defaultMessage: 'Search components', + defaultMessage: 'Search component templates', } ), }; @@ -78,24 +85,33 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro return []; } - return components.filter((component) => { - if (filters.settings.checked === 'on' && !component.hasSettings) { - return false; - } - if (filters.mappings.checked === 'on' && !component.hasMappings) { - return false; - } - if (filters.aliases.checked === 'on' && !component.hasAliases) { - return false; - } - - if (searchValue.trim() === '') { - return true; - } - - const match = fuzzyMatch(searchValue, component.name); - return match; - }); + return components + .filter((component) => { + if (filters.settings.checked === 'on' && !component.hasSettings) { + return false; + } + if (filters.mappings.checked === 'on' && !component.hasMappings) { + return false; + } + if (filters.aliases.checked === 'on' && !component.hasAliases) { + return false; + } + + if (searchValue.trim() === '') { + return true; + } + + const match = fuzzyMatch(searchValue, component.name); + return match; + }) + .sort((a, b) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } + return 0; + }); }, [isLoading, components, searchValue, filters]); const isSearchResultEmpty = filteredComponents.length === 0 && components.length > 0; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss index 6abbbe65790e7..61d5512da2cd9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss @@ -32,5 +32,9 @@ font-weight: 600; } } + + &__content { + mask-image: none; + } } } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx index af48c3c79379a..8795c08fd2bee 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx @@ -96,7 +96,7 @@ export const ComponentTemplatesSelector = ({ ); @@ -136,7 +136,7 @@ export const ComponentTemplatesSelector = ({ }} /> -
+
)} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx index 8762eae9d2297..18988fa125a06 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx @@ -200,7 +200,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( } > - {isMetaVisible ? ( + {isMetaVisible && ( = React.memo( 'aria-label': i18n.translate( 'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaAriaLabel', { - defaultMessage: 'Metadata JSON editor', + defaultMessage: '_meta field data editor', } ), }, }} /> - ) : ( - // requires children or a field - // For now, we return an empty
if the editor is not visible -
)} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx index 0c52037abde45..c577957339487 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx @@ -65,7 +65,7 @@ export const logisticsFormSchema: FormSchema = { }, _meta: { label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', { - defaultMessage: 'Metadata (optional)', + defaultMessage: '_meta field data (optional)', }), helpText: ( - Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; - const formSerializer: SerializerFunc = (formData) => { const { dynamicMapping: { @@ -40,22 +37,17 @@ const formSerializer: SerializerFunc = (formData) => { const dynamic = dynamicMappingsEnabled ? true : throwErrorsForUnmappedFields ? 'strict' : false; - let parsedMeta; - try { - parsedMeta = JSON.parse(metaField); - } catch { - parsedMeta = {}; - } - - return { + const serialized = { dynamic, numeric_detection, date_detection, dynamic_date_formats, - _source: { ...sourceField }, - _meta: parsedMeta, + _source: sourceField, + _meta: metaField, _routing, }; + + return serialized; }; const formDeserializer = (formData: GenericObject) => { @@ -64,7 +56,11 @@ const formDeserializer = (formData: GenericObject) => { numeric_detection, date_detection, dynamic_date_formats, - _source: { enabled, includes, excludes }, + _source: { enabled, includes, excludes } = {} as { + enabled?: boolean; + includes?: string[]; + excludes?: string[]; + }, _meta, _routing, } = formData; @@ -82,7 +78,7 @@ const formDeserializer = (formData: GenericObject) => { includes, excludes, }, - metaField: stringifyJson(_meta), + metaField: _meta ?? {}, _routing, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index c06340fd9ae14..6e80f8b813ec2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -48,10 +48,30 @@ export const configurationFormSchema: FormSchema = { validator: isJsonField( i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.metaFieldEditorJsonError', { defaultMessage: 'The _meta field JSON is not valid.', - }) + }), + { allowEmptyString: true } ), }, ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value, null, 2); + }, + serializer: (value: string) => { + try { + const parsed = JSON.parse(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(parsed).length) { + return undefined; + } + return parsed; + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, }, sourceField: { enabled: { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index 80937e7da1192..79685d46b6bdd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -22,7 +22,7 @@ interface Props { const stringifyJson = (json: { [key: string]: any }) => Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; -const formSerializer: SerializerFunc = (formData) => { +const formSerializer: SerializerFunc = (formData) => { const { dynamicTemplates } = formData; let parsedTemplates; @@ -34,12 +34,14 @@ const formSerializer: SerializerFunc = (formData) => { parsedTemplates = [parsedTemplates]; } } catch { - parsedTemplates = []; + // Silently swallow errors } - return { - dynamic_templates: parsedTemplates, - }; + return Array.isArray(parsedTemplates) && parsedTemplates.length > 0 + ? { + dynamic_templates: parsedTemplates, + } + : undefined; }; const formDeserializer = (formData: { [key: string]: any }) => { @@ -53,7 +55,7 @@ const formDeserializer = (formData: { [key: string]: any }) => { export const TemplatesForm = React.memo(({ value }: Props) => { const isMounted = useRef(undefined); - const { form } = useForm({ + const { form } = useForm({ schema: templatesFormSchema, serializer: formSerializer, deserializer: formDeserializer, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index 9fa4a7981c047..8b3ff60005305 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -199,7 +199,7 @@ export const getTypeMetaFromSource = ( * * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types) */ -export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { +export const normalize = (fieldsToNormalize: Fields = {}): NormalizedFields => { let maxNestedDepth = 0; const normalizeFields = ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 46dc1176f62b4..e8fda90737708 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -39,14 +39,14 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr } const { - _source = {}, - _meta = {}, + _source, + _meta, _routing, dynamic, numeric_detection, date_detection, dynamic_date_formats, - properties = {}, + properties, dynamic_templates, } = mappingsDefinition; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx index fb4bfae974000..ad5056fa73ce1 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx @@ -19,7 +19,7 @@ import { normalize, deNormalize, stripUndefinedValues } from './lib'; type Mappings = MappingsTemplates & MappingsConfiguration & { - properties: MappingsFields; + properties?: MappingsFields; }; export interface Types { @@ -31,7 +31,7 @@ export interface Types { export interface OnUpdateHandlerArg { isValid?: boolean; - getData: () => Mappings; + getData: () => Mappings | undefined; validate: () => Promise; } @@ -114,13 +114,18 @@ export const MappingsState = React.memo(({ children, onChange, value }: Props) = const configurationData = state.configuration.data.format(); const templatesData = state.templates.data.format(); - return { + const output = { ...stripUndefinedValues({ ...configurationData, ...templatesData, }), - properties: fields, }; + + if (fields && Object.keys(fields).length > 0) { + output.properties = fields; + } + + return Object.keys(output).length > 0 ? (output as Mappings) : undefined; }, validate: async () => { const configurationFormValidator = diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx index 01771f40f89ea..df0cc791384fe 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx @@ -25,6 +25,12 @@ interface Props { } const i18nTexts = { + title: ( + + ), description: ( { - onChange({ isValid: true, validate: async () => true, getData: () => components }); + onChange({ + isValid: true, + validate: async () => true, + getData: () => (components.length > 0 ? components : undefined), + }); }, [onChange] ); @@ -63,12 +73,7 @@ export const StepComponents = ({ defaultValue = [], onChange, esDocsBase }: Prop -

- -

+

{i18nTexts.title}

diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index 44ec4db0873f3..2777941175429 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiLink, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -16,6 +23,7 @@ import { Field, Forms, JsonEditorField, + FormDataProvider, } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -24,70 +32,125 @@ import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_f const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); -const fieldsMeta = { - name: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameTitle', { - defaultMessage: 'Name', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameDescription', { - defaultMessage: 'A unique identifier for this template.', - }), - testSubject: 'nameField', - }, - indexPatterns: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle', { - defaultMessage: 'Index patterns', - }), - description: i18n.translate( - 'xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription', - { - defaultMessage: 'The index patterns to apply to the template.', - } - ), - testSubject: 'indexPatternsField', - }, - order: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', { - defaultMessage: 'Merge order', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderDescription', { - defaultMessage: 'The merge order when multiple templates match an index.', - }), - testSubject: 'orderField', - }, - priority: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', { - defaultMessage: 'Merge priority', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', { - defaultMessage: 'The merge priority when multiple templates match an index.', - }), - testSubject: 'priorityField', - }, - version: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', { - defaultMessage: 'Version', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionDescription', { - defaultMessage: 'A number that identifies the template to external management systems.', - }), - testSubject: 'versionField', - }, -}; +function getFieldsMeta(esDocsBase: string) { + return { + name: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameTitle', { + defaultMessage: 'Name', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameDescription', { + defaultMessage: 'A unique identifier for this template.', + }), + testSubject: 'nameField', + }, + indexPatterns: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle', { + defaultMessage: 'Index patterns', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription', + { + defaultMessage: 'The index patterns to apply to the template.', + } + ), + testSubject: 'indexPatternsField', + }, + dataStream: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.dataStreamTitle', { + defaultMessage: 'Data stream', + }), + description: ( + +
+ + {i18n.translate( + 'xpack.idxMgmt.templateForm.stepLogistics.dataStreamDocumentionLink', + { + defaultMessage: 'Learn more about data streams.', + } + )} + + + ), + }} + /> + ), + testSubject: 'dataStreamField', + }, + order: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', { + defaultMessage: 'Merge order', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderDescription', { + defaultMessage: 'The merge order when multiple templates match an index.', + }), + testSubject: 'orderField', + }, + priority: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', { + defaultMessage: 'Priority', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', { + defaultMessage: 'Only the highest priority template will be applied.', + }), + testSubject: 'priorityField', + }, + version: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', { + defaultMessage: 'Version', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionDescription', { + defaultMessage: 'A number that identifies the template to external management systems.', + }), + testSubject: 'versionField', + }, + }; +} + +interface LogisticsForm { + [key: string]: any; +} + +interface LogisticsFormInternal extends LogisticsForm { + __internal__: { + addMeta: boolean; + }; +} interface Props { - defaultValue: { [key: string]: any }; + defaultValue: LogisticsForm; onChange: (content: Forms.Content) => void; isEditing?: boolean; isLegacy?: boolean; } +function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { + return { + ...formData, + __internal__: { + addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), + }, + }; +} + +function formSerializer(formData: LogisticsFormInternal): LogisticsForm { + const { __internal__, ...rest } = formData; + return rest; +} + export const StepLogistics: React.FunctionComponent = React.memo( ({ defaultValue, isEditing = false, onChange, isLegacy = false }) => { const { form } = useForm({ schema: schemas.logistics, defaultValue, options: { stripEmptyFields: false }, + serializer: formSerializer, + deserializer: formDeserializer, }); /** @@ -117,7 +180,9 @@ export const StepLogistics: React.FunctionComponent = React.memo( return subscription.unsubscribe; }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps - const { name, indexPatterns, order, priority, version } = fieldsMeta; + const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( + documentationService.getEsDocsBase() + ); return ( <> @@ -180,6 +245,16 @@ export const StepLogistics: React.FunctionComponent = React.memo( /> + {/* Create data stream */} + {isLegacy !== true && ( + + + + )} + {/* Order */} {isLegacy && ( @@ -226,25 +301,35 @@ export const StepLogistics: React.FunctionComponent = React.memo( id="xpack.idxMgmt.templateForm.stepLogistics.metaFieldDescription" defaultMessage="Use the _meta field to store any metadata you want." /> + + } > - + {({ '__internal__.addMeta': addMeta }) => { + return ( + addMeta && ( + + ) + ); }} - /> + )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 880c7fbd7f23c..0f4b9de4f6cfa 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -168,7 +168,7 @@ export const StepReview: React.FunctionComponent = React.memo( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 6310ac09488e5..f5c9be9292cd0 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -50,7 +50,7 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { components: { id: 'components', label: i18n.translate('xpack.idxMgmt.templateForm.steps.componentsStepName', { - defaultMessage: 'Components', + defaultMessage: 'Component templates', }), }, settings: { @@ -91,15 +91,9 @@ export const TemplateForm = ({ const indexTemplate = defaultValue ?? { name: '', indexPatterns: [], - composedOf: [], - template: { - settings: {}, - mappings: {}, - aliases: {}, - }, + template: {}, _kbnMeta: { - isManaged: false, - isCloudManaged: false, + type: 'default', hasDatastream: false, isLegacy, }, @@ -150,18 +144,50 @@ export const TemplateForm = ({ ) : null; - const buildTemplateObject = (initialTemplate: TemplateDeserialized) => ( - wizardData: WizardContent - ): TemplateDeserialized => ({ - ...initialTemplate, - ...wizardData.logistics, - composedOf: wizardData.components, - template: { - settings: wizardData.settings, - mappings: wizardData.mappings, - aliases: wizardData.aliases, + /** + * If no mappings, settings or aliases are defined, it is better to not send empty + * object for those values. + * This method takes care of that and other cleanup of empty fields. + * @param template The template object to clean up + */ + const cleanupTemplateObject = (template: TemplateDeserialized) => { + const outputTemplate = { ...template }; + + if (outputTemplate.template.settings === undefined) { + delete outputTemplate.template.settings; + } + if (outputTemplate.template.mappings === undefined) { + delete outputTemplate.template.mappings; + } + if (outputTemplate.template.aliases === undefined) { + delete outputTemplate.template.aliases; + } + if (Object.keys(outputTemplate.template).length === 0) { + delete outputTemplate.template; + } + + return outputTemplate; + }; + + const buildTemplateObject = useCallback( + (initialTemplate: TemplateDeserialized) => ( + wizardData: WizardContent + ): TemplateDeserialized => { + const outputTemplate = { + ...initialTemplate, + ...wizardData.logistics, + composedOf: wizardData.components, + template: { + settings: wizardData.settings, + mappings: wizardData.mappings, + aliases: wizardData.aliases, + }, + }; + + return cleanupTemplateObject(outputTemplate); }, - }); + [] + ); const onSaveTemplate = useCallback( async (wizardData: WizardContent) => { @@ -177,7 +203,7 @@ export const TemplateForm = ({ clearSaveError(); }, - [indexTemplate, onSave, clearSaveError] + [indexTemplate, buildTemplateObject, onSave, clearSaveError] ); return ( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 5af3b4dd00c4f..d8c3ad8c259fc 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -128,6 +128,32 @@ export const schemas: Record = { }, ], }, + dataStream: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.datastreamLabel', { + defaultMessage: 'Create data stream', + }), + defaultValue: false, + serializer: (value) => { + if (value === true) { + return { + timestamp_field: '@timestamp', + }; + } + }, + deserializer: (value) => { + if (typeof value === 'boolean') { + return value; + } + + /** + * For now, it is enough to have a "data_stream" declared on the index template + * to assume that the template creates a data stream. In the future, this condition + * might change + */ + return value !== undefined; + }, + }, order: { type: FIELD_TYPES.NUMBER, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldOrderLabel', { @@ -187,5 +213,13 @@ export const schemas: Record = { } }, }, + __internal__: { + addMeta: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { + defaultMessage: 'Add metadata', + }), + }, + }, }, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 7bd04cdbf0c91..ee8970a3c4509 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -144,7 +144,6 @@ export const IndexManagementHome: React.FunctionComponent - index.name); const selectedIndexNames = Object.keys(selectedIndicesMap); diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts index 156d792c26f1d..3954ce04ca0b5 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts @@ -5,3 +5,5 @@ */ export * from './filter_list_button'; + +export * from './template_type_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx new file mode 100644 index 0000000000000..c6b0e21ebfdc1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.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 from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; + +import { TemplateType } from '../../../../../../common'; + +interface Props { + templateType: TemplateType; +} + +const i18nTexts = { + managed: i18n.translate('xpack.idxMgmt.templateBadgeType.managed', { + defaultMessage: 'Managed', + }), + cloudManaged: i18n.translate('xpack.idxMgmt.templateBadgeType.cloudManaged', { + defaultMessage: 'Cloud-managed', + }), + system: i18n.translate('xpack.idxMgmt.templateBadgeType.system', { defaultMessage: 'System' }), +}; + +export const TemplateTypeIndicator = ({ templateType }: Props) => { + if (templateType === 'default') { + return null; + } + + return ( + + {i18nTexts[templateType]} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index b470bcfd7660e..9203e76fce787 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -7,7 +7,7 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiInMemoryTable, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { SendRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports'; import { TemplateListItem } from '../../../../../../../common'; @@ -15,6 +15,8 @@ import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/con import { TemplateDeleteModal } from '../../../../../components'; import { encodePathForReactRouter } from '../../../../../services/routing'; import { useServices } from '../../../../../app_context'; +import { TemplateContentIndicator } from '../../../../../components/shared'; +import { TemplateTypeIndicator } from '../../components'; interface Props { templates: TemplateListItem[]; @@ -47,20 +49,23 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ sortable: true, render: (name: TemplateListItem['name'], item: TemplateListItem) => { return ( - /* eslint-disable-next-line @elastic/eui/href-or-on-click */ - uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) - )} - data-test-subj="templateDetailsLink" - > - {name} - + <> + uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) + )} + data-test-subj="templateDetailsLink" + > + {name} + +   + + ); }, }, @@ -98,44 +103,30 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ ) : null, }, { - field: 'order', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.orderColumnTitle', { - defaultMessage: 'Order', - }), - truncateText: true, - sortable: true, - }, - { - field: 'hasMappings', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.mappingsColumnTitle', { - defaultMessage: 'Mappings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.contentColumnTitle', { + defaultMessage: 'Content', }), - truncateText: true, - sortable: true, - render: (hasMappings: boolean) => (hasMappings ? : null), - }, - { - field: 'hasSettings', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.settingsColumnTitle', { - defaultMessage: 'Settings', - }), - truncateText: true, - sortable: true, - render: (hasSettings: boolean) => (hasSettings ? : null), - }, - { - field: 'hasAliases', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.aliasesColumnTitle', { - defaultMessage: 'Aliases', - }), - truncateText: true, - sortable: true, - render: (hasAliases: boolean) => (hasAliases ? : null), + width: '120px', + render: (item: TemplateListItem) => ( + + {i18n.translate('xpack.idxMgmt.templateList.table.noneDescriptionText', { + defaultMessage: 'None', + })} + + } + /> + ), }, { name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionColumnTitle', { defaultMessage: 'Actions', }), + width: '120px', actions: [ { name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionEditText', { @@ -153,7 +144,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name, true); }, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, { type: 'icon', @@ -188,7 +179,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ setTemplatesToDelete([{ name, isLegacy }]); }, isPrimary: true, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, ], }, @@ -208,13 +199,13 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ const selectionConfig = { onSelectionChange: setSelection, - selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + selectable: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( - 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + 'xpack.idxMgmt.templateList.legacyTable.deleteCloudManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', + defaultMessage: 'You cannot delete a cloud-managed template.', } ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index fe6c9ad3d8e07..0c403e69d2e76 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -17,9 +17,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiCodeBlock, + EuiSpacer, } from '@elastic/eui'; +import { useAppContext } from '../../../../../app_context'; import { TemplateDeserialized } from '../../../../../../../common'; -import { getILMPolicyPath } from '../../../../../services/navigation'; +import { getILMPolicyPath } from '../../../../../services/routing'; interface Props { templateDetails: TemplateDeserialized; @@ -51,158 +53,174 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) const numIndexPatterns = indexPatterns.length; + const { + core: { getUrlForApp }, + } = useAppContext(); + return ( - - - - {/* Index patterns */} - - - - - {numIndexPatterns > 1 ? ( - -
    - {indexPatterns.map((indexName: string, i: number) => { - return ( -
  • - - {indexName} - -
  • - ); - })} -
-
+ <> + + + + {/* Index patterns */} + + + + + {numIndexPatterns > 1 ? ( + +
    + {indexPatterns.map((indexName: string, i: number) => { + return ( +
  • + + {indexName} + +
  • + ); + })} +
+
+ ) : ( + indexPatterns.toString() + )} +
+ + {/* Priority / Order */} + {isLegacy !== true ? ( + <> + + + + + {priority || priority === 0 ? priority : i18nTexts.none} + + ) : ( - indexPatterns.toString() + <> + + + + + {order || order === 0 ? order : i18nTexts.none} + + )} -
- - {/* Priority / Order */} - {isLegacy !== true ? ( - <> - - - - - {priority || priority === 0 ? priority : i18nTexts.none} - - - ) : ( - <> - - - - - {order || order === 0 ? order : i18nTexts.none} - - - )} - {/* Components */} - {isLegacy !== true && ( - <> - - - - - {composedOf && composedOf.length > 0 ? ( -
    - {composedOf.map((component) => ( -
  • - - {component} - -
  • - ))} -
- ) : ( - i18nTexts.none - )} -
- - )} -
-
+ {/* Components */} + {isLegacy !== true && ( + <> + + + + + {composedOf && composedOf.length > 0 ? ( +
    + {composedOf.map((component) => ( +
  • + + {component} + +
  • + ))} +
+ ) : ( + i18nTexts.none + )} +
+ + )} + +
- - - {/* ILM Policy (only for legacy as composable template could have ILM policy + + + {/* ILM Policy (only for legacy as composable template could have ILM policy inside one of their components) */} - {isLegacy && ( - <> - - - - - {ilmPolicy && ilmPolicy.name ? ( - {ilmPolicy.name} - ) : ( - i18nTexts.none - )} - - - )} + {isLegacy && ( + <> + + + + + {ilmPolicy && ilmPolicy.name ? ( + + {ilmPolicy.name} + + ) : ( + i18nTexts.none + )} + + + )} + + {/* Has data stream? (only for composable template) */} + {isLegacy !== true && ( + <> + + + + + {hasDatastream ? i18nTexts.yes : i18nTexts.no} + + + )} - {/* Has data stream? (only for composable template) */} - {isLegacy !== true && ( - <> - - - - - {hasDatastream ? i18nTexts.yes : i18nTexts.no} - - - )} + {/* Version */} + + + + + {version || version === 0 ? version : i18nTexts.none} + + + +
- {/* Version */} - - - - - {version || version === 0 ? version : i18nTexts.none} - + - {/* Metadata (optional) */} - {isLegacy !== true && _meta && ( - <> - - - - - {JSON.stringify(_meta, null, 2)} - - - )} - - - + + {/* Metadata (optional) */} + {isLegacy !== true && _meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )} + + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index 34e90aef51701..5b726013a1d92 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -36,6 +36,7 @@ import { useLoadIndexTemplate } from '../../../../services/api'; import { decodePathFromReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; +import { TemplateTypeIndicator } from '../components'; import { TabSummary } from './tabs'; const SUMMARY_TAB_ID = 'summary'; @@ -98,7 +99,7 @@ export const TemplateDetailsContent = ({ decodedTemplateName, isLegacy ); - const isCloudManaged = templateDetails?._kbnMeta.isCloudManaged ?? false; + const isCloudManaged = templateDetails?._kbnMeta.type === 'cloudManaged'; const [templateToDelete, setTemplateToDelete] = useState< Array<{ name: string; isLegacy?: boolean }> >([]); @@ -111,6 +112,12 @@ export const TemplateDetailsContent = ({

{decodedTemplateName} + {templateDetails && ( + <> +   + + + )}

@@ -163,16 +170,16 @@ export const TemplateDetailsContent = ({ } color="primary" size="s" > diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index 18a65407ee20d..f421bc5d87a54 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -37,13 +37,19 @@ import { TemplateDetails } from './template_details'; import { LegacyTemplateTable } from './legacy_templates/template_table'; import { FilterListButton, Filters } from './components'; -type FilterName = 'composable' | 'system'; +type FilterName = 'managed' | 'cloudManaged' | 'system'; interface MatchParams { templateName?: string; } -const stripOutSystemTemplates = (templates: TemplateListItem[]): TemplateListItem[] => - templates.filter((template) => !template.name.startsWith('.')); +function filterTemplates(templates: TemplateListItem[], types: string[]): TemplateListItem[] { + return templates.filter((template) => { + if (template._kbnMeta.type === 'default') { + return true; + } + return types.includes(template._kbnMeta.type); + }); +} export const TemplateList: React.FunctionComponent> = ({ match: { @@ -56,12 +62,18 @@ export const TemplateList: React.FunctionComponent>({ - composable: { - name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewComposableTemplateLabel', { - defaultMessage: 'Composable templates', + managed: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewManagedTemplateLabel', { + defaultMessage: 'Managed templates', }), checked: 'on', }, + cloudManaged: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewCloudManagedTemplateLabel', { + defaultMessage: 'Cloud-managed templates', + }), + checked: 'off', + }, system: { name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewSystemTemplateLabel', { defaultMessage: 'System templates', @@ -72,18 +84,19 @@ export const TemplateList: React.FunctionComponent { if (!allTemplates) { + // If templates are not fetched, return empty arrays. return { templates: [], legacyTemplates: [] }; } - return filters.system.checked === 'on' - ? allTemplates - : { - templates: stripOutSystemTemplates(allTemplates.templates), - legacyTemplates: stripOutSystemTemplates(allTemplates.legacyTemplates), - }; - }, [allTemplates, filters.system.checked]); + const visibleTemplateTypes = Object.entries(filters) + .filter(([name, _filter]) => _filter.checked === 'on') + .map(([name]) => name); - const showComposableTemplateTable = filters.composable.checked === 'on'; + return { + templates: filterTemplates(allTemplates.templates, visibleTemplateTypes), + legacyTemplates: filterTemplates(allTemplates.legacyTemplates, visibleTemplateTypes), + }; + }, [allTemplates, filters]); const selectedTemplate = Boolean(templateName) ? { @@ -154,8 +167,8 @@ export const TemplateList: React.FunctionComponent ); - const renderTemplatesTable = () => - showComposableTemplateTable ? ( + const renderTemplatesTable = () => { + return ( <> - ) : null; + ); + }; const renderLegacyTemplatesTable = () => ( <> diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx index 55a777363d06f..3dffdcde160f1 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx @@ -7,14 +7,7 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiInMemoryTable, - EuiBasicTableColumn, - EuiButton, - EuiLink, - EuiBadge, - EuiIcon, -} from '@elastic/eui'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink, EuiIcon } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateListItem } from '../../../../../../common'; @@ -24,6 +17,7 @@ import { encodePathForReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TemplateDeleteModal } from '../../../../components'; import { TemplateContentIndicator } from '../../../../components/shared'; +import { TemplateTypeIndicator } from '../components'; interface Props { templates: TemplateListItem[]; @@ -70,13 +64,7 @@ export const TemplateTable: React.FunctionComponent = ({ {name}   - {item._kbnMeta.isManaged ? ( - - Managed - - ) : ( - '' - )} + ); }, @@ -99,14 +87,6 @@ export const TemplateTable: React.FunctionComponent = ({ sortable: true, render: (composedOf: string[] = []) => {composedOf.join(', ')}, }, - { - field: 'priority', - name: i18n.translate('xpack.idxMgmt.templateList.table.priorityColumnTitle', { - defaultMessage: 'Priority', - }), - truncateText: true, - sortable: true, - }, { name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', { defaultMessage: 'Data stream', @@ -119,7 +99,7 @@ export const TemplateTable: React.FunctionComponent = ({ name: i18n.translate('xpack.idxMgmt.templateList.table.contentColumnTitle', { defaultMessage: 'Content', }), - truncateText: true, + width: '120px', render: (item: TemplateListItem) => ( = ({ name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', { defaultMessage: 'Actions', }), + width: '120px', actions: [ { name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', { @@ -153,7 +134,7 @@ export const TemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name); }, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, { type: 'icon', @@ -182,7 +163,7 @@ export const TemplateTable: React.FunctionComponent = ({ setTemplatesToDelete([{ name, isLegacy }]); }, isPrimary: true, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, ], }, @@ -202,13 +183,13 @@ export const TemplateTable: React.FunctionComponent = ({ const selectionConfig = { onSelectionChange: setSelection, - selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + selectable: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( - 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + 'xpack.idxMgmt.templateList.table.deleteCloudManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', + defaultMessage: 'You cannot delete a cloud-managed template.', } ); } diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 6ecefe18b1a61..29fd2e02120fc 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -85,11 +85,11 @@ export const TemplateEdit: React.FunctionComponent { - if (filter) { - // React router tries to decode url params but it can't because the browser partially - // decodes them. So we have to encode both the URL and the filter to get it all to - // work correctly for filters with URL unsafe characters in them. - return encodeURI(`/indices/filter/${encodeURIComponent(filter)}`); - } - - // If no filter, URI is already safe so no need to encode. - return '/indices'; -}; - -export const getILMPolicyPath = (policyName: string) => { - return encodeURI(`/policies/edit/${encodeURIComponent(policyName)}`); -}; diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index 8831fa2368f47..68bf06409e6ab 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -31,6 +31,28 @@ export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { return encodeURI(url); }; +export const getILMPolicyPath = (policyName: string) => { + return encodeURI( + `/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}` + ); +}; + +export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean) => { + const hiddenIndicesParam = + typeof includeHiddenIndices !== 'undefined' ? includeHiddenIndices : false; + if (filter) { + // React router tries to decode url params but it can't because the browser partially + // decodes them. So we have to encode both the URL and the filter to get it all to + // work correctly for filters with URL unsafe characters in them. + return encodeURI( + `/indices?includeHiddenIndices=${hiddenIndicesParam}&filter=${encodeURIComponent(filter)}` + ); + } + + // If no filter, URI is already safe so no need to encode. + return '/indices'; +}; + export const decodePathFromReactRouter = (pathname: string): string => { let decodedPath; try { diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts index 7a76fff7f3ec6..a2e9a41feb165 100644 --- a/x-pack/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -13,4 +13,4 @@ export const plugin = () => { export { IndexManagementPluginSetup }; -export { getIndexListUri } from './application/services/navigation'; +export { getIndexListUri } from './application/services/routing'; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 5bf1a31d0902b..3f7fcf424f1f0 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -22,6 +22,8 @@ export { useForm, Form, getUseField, + UseField, + FormDataProvider, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { @@ -33,6 +35,7 @@ export { export { getFormRow, Field, + ToggleField, JsonEditorField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 5f4e625348333..b91c7b4650180 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -17,7 +17,9 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou const { callAsCurrentUser } = ctx.dataManagement!.client; try { - const dataStreams = await callAsCurrentUser('dataManagement.getDataStreams'); + const { data_streams: dataStreams } = await callAsCurrentUser( + 'dataManagement.getDataStreams' + ); const body = deserializeDataStreamList(dataStreams); return res.ok({ body }); @@ -50,7 +52,10 @@ export function registerGetOneRoute({ router, license, lib: { isEsError } }: Rou const { callAsCurrentUser } = ctx.dataManagement!.client; try { - const dataStream = await callAsCurrentUser('dataManagement.getDataStream', { name }); + const { data_streams: dataStream } = await callAsCurrentUser( + 'dataManagement.getDataStream', + { name } + ); if (dataStream[0]) { const body = deserializeDataStream(dataStream[0]); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index c905f92d70541..18c74716a35b6 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -20,6 +20,7 @@ export const templateSchema = schema.object({ }) ), composedOf: schema.maybe(schema.arrayOf(schema.string())), + dataStream: schema.maybe(schema.object({}, { unknowns: 'allow' })), _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), ilmPolicy: schema.maybe( schema.object({ @@ -28,8 +29,7 @@ export const templateSchema = schema.object({ }) ), _kbnMeta: schema.object({ - isManaged: schema.maybe(schema.boolean()), - isCloudManaged: schema.maybe(schema.boolean()), + type: schema.string(), hasDatastream: schema.maybe(schema.boolean()), isLegacy: schema.maybe(schema.boolean()), }), diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 1a44ac0f71f20..3b9de2b3409b6 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -5,7 +5,11 @@ */ import { getRandomString, getRandomNumber } from '../../../../test_utils'; -import { TemplateDeserialized } from '../../common'; +import { TemplateDeserialized, TemplateType, TemplateListItem } from '../../common'; + +const objHasProperties = (obj?: Record): boolean => { + return obj === undefined || Object.keys(obj).length === 0 ? false : true; +}; export const getTemplate = ({ name = getRandomString(), @@ -13,31 +17,35 @@ export const getTemplate = ({ order = getRandomNumber(), indexPatterns = [], template: { settings, aliases, mappings } = {}, - isManaged = false, - isCloudManaged = false, hasDatastream = false, isLegacy = false, + type = 'default', }: Partial< TemplateDeserialized & { isLegacy?: boolean; - isManaged: boolean; - isCloudManaged: boolean; + type?: TemplateType; hasDatastream: boolean; } -> = {}): TemplateDeserialized => ({ - name, - version, - order, - indexPatterns, - template: { - aliases, - mappings, - settings, - }, - _kbnMeta: { - isManaged, - isCloudManaged, - hasDatastream, - isLegacy, - }, -}); +> = {}): TemplateDeserialized & TemplateListItem => { + const indexTemplate = { + name, + version, + order, + indexPatterns, + template: { + aliases, + mappings, + settings, + }, + hasSettings: objHasProperties(settings), + hasMappings: objHasProperties(mappings), + hasAliases: objHasProperties(aliases), + _kbnMeta: { + type, + hasDatastream, + isLegacy, + }, + }; + + return indexTemplate; +}; diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap index 99ab129fc36e3..4680414493a2c 100644 --- a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap +++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap @@ -91,7 +91,6 @@ Object { "y": 3.5, }, ], - "label": "Inbound traffic", }, "outboundTraffic": Object { "coordinates": Array [ @@ -180,32 +179,26 @@ Object { "y": 4, }, ], - "label": "Outbound traffic", }, }, "stats": Object { "cpu": Object { - "label": "CPU usage", "type": "percent", "value": 0.0015, }, "hosts": Object { - "label": "Hosts", "type": "number", "value": 2, }, "inboundTraffic": Object { - "label": "Inbound traffic", "type": "bytesPerSecond", "value": 3.5, }, "memory": Object { - "label": "Memory usage", "type": "percent", "value": 0.0015, }, "outboundTraffic": Object { - "label": "Outbound traffic", "type": "bytesPerSecond", "value": 3, }, diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts index 15751fab39abc..25b334d03c4f7 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts @@ -103,14 +103,6 @@ export const createMetricsFetchData = ( body: JSON.stringify(snapshotRequest), }); - const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', { - defaultMessage: 'Inbound traffic', - }); - - const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', { - defaultMessage: 'Outbound traffic', - }); - return { title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', { defaultMessage: 'Metrics', @@ -119,43 +111,30 @@ export const createMetricsFetchData = ( stats: { hosts: { type: 'number', - label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', { - defaultMessage: 'Hosts', - }), value: results.nodes.length, }, cpu: { type: 'percent', - label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', { - defaultMessage: 'CPU usage', - }), value: combineNodesBy('cpu', results.nodes, average), }, memory: { type: 'percent', - label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', { - defaultMessage: 'Memory usage', - }), value: combineNodesBy('memory', results.nodes, average), }, inboundTraffic: { type: 'bytesPerSecond', - label: inboundLabel, value: combineNodesBy('rx', results.nodes, average), }, outboundTraffic: { type: 'bytesPerSecond', - label: outboundLabel, value: combineNodesBy('tx', results.nodes, average), }, }, series: { inboundTraffic: { - label: inboundLabel, coordinates: combineNodeTimeseriesBy('rx', results.nodes, average), }, outboundTraffic: { - label: outboundLabel, coordinates: combineNodeTimeseriesBy('tx', results.nodes, average), }, }, diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index b1d92d3a78e65..6489c30308771 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -5,63 +5,52 @@ */ import { - AGENT_TYPE_TEMPORARY, AGENT_POLLING_THRESHOLD_MS, AGENT_TYPE_PERMANENT, - AGENT_TYPE_EPHEMERAL, AGENT_SAVED_OBJECT_TYPE, } from '../constants'; import { Agent, AgentStatus } from '../types'; export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { - const { type, last_checkin: lastCheckIn } = agent; - const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); - const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn; - const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS); + const { last_checkin: lastCheckIn } = agent; + if (!agent.active) { return 'inactive'; } + if (!agent.last_checkin) { + return 'enrolling'; + } if (agent.unenrollment_started_at && !agent.unenrolled_at) { return 'unenrolling'; } - if (agent.current_error_events.length > 0) { + + const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); + const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn; + const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS); + + if (agent.last_checkin_status === 'error') { return 'error'; } - switch (type) { - case AGENT_TYPE_PERMANENT: - if (intervalsSinceLastCheckIn >= 4) { - return 'error'; - } - case AGENT_TYPE_TEMPORARY: - if (intervalsSinceLastCheckIn >= 3) { - return 'offline'; - } - case AGENT_TYPE_EPHEMERAL: - if (intervalsSinceLastCheckIn >= 3) { - return 'inactive'; - } + if (agent.last_checkin_status === 'degraded') { + return 'degraded'; + } + if (intervalsSinceLastCheckIn >= 4) { + return 'offline'; } + return 'online'; } export function buildKueryForOnlineAgents() { - return `(${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${ - (4 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s) or (${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${ - (3 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s) or (${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_EPHEMERAL} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${ - (3 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s)`; + return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()})`; } -export function buildKueryForOfflineAgents() { - return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ - (3 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s`; +export function buildKueryForErrorAgents() { + return `( ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded )`; } -export function buildKueryForErrorAgents() { - return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ +export function buildKueryForOfflineAgents() { + return `((${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ (4 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s`; + }s) AND not ( ${buildKueryForErrorAgents()} ))`; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 1f4718acc2c1f..d3789c58a2c22 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -11,7 +11,16 @@ export type AgentType = | typeof AGENT_TYPE_PERMANENT | typeof AGENT_TYPE_TEMPORARY; -export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning' | 'unenrolling'; +export type AgentStatus = + | 'offline' + | 'error' + | 'online' + | 'inactive' + | 'warning' + | 'enrolling' + | 'unenrolling' + | 'degraded'; + export type AgentActionType = 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE' | 'UNENROLL'; export interface NewAgentAction { type: AgentActionType; @@ -82,6 +91,7 @@ interface AgentBase { config_id?: string; config_revision?: number | null; last_checkin?: string; + last_checkin_status?: 'error' | 'online' | 'degraded'; user_provided_metadata: AgentMetadata; local_metadata: AgentMetadata; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 1105c8ee7ca82..ed7d73ab0b719 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -47,6 +47,7 @@ export interface PostAgentCheckinRequest { agentId: string; }; body: { + status?: 'online' | 'error' | 'degraded'; local_metadata?: Record; events?: NewAgentEvent[]; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 30204603e764c..36a8bf908ddd7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -178,11 +178,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { } if (selectedStatus.length) { - if (kuery) { - kuery = `(${kuery}) and`; - } - - kuery = selectedStatus + const kueryStatus = selectedStatus .map((status) => { switch (status) { case 'online': @@ -196,6 +192,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return ''; }) .join(' or '); + + if (kuery) { + kuery = `(${kuery}) and ${kueryStatus}`; + } else { + kuery = kueryStatus; + } } const agentsRequest = useGetAgents( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx index e4dfa520259eb..7c6c95cab420f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx @@ -53,6 +53,22 @@ const Status = { /> ), + Degraded: ( + + + + ), + Enrolling: ( + + + + ), Unenrolling: ( = {}; + const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events); if (updatedErrorEvents) { updateData.current_error_events = JSON.stringify(updatedErrorEvents); } - if (localMetadata) { - updateData.local_metadata = localMetadata; + if (data.localMetadata) { + updateData.local_metadata = data.localMetadata; + } + + if (data.status !== agent.last_checkin_status) { + updateData.last_checkin_status = data.status; } if (Object.keys(updateData).length > 0) { await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts index 96e006b78f00f..994ecc64c82a7 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts @@ -59,7 +59,7 @@ export function agentCheckinStateConnectedAgentsFactory() { const internalSOClient = getInternalUserSOClient(); const now = new Date().toISOString(); const updates: Array> = [ - ...connectedAgentsIds.values(), + ...agentToUpdate.values(), ].map((agentId) => ({ type: AGENT_SAVED_OBJECT_TYPE, id: agentId, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts index 8140b1e6de470..f216cd541eb21 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -33,6 +33,7 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: true, + last_checkin: new Date().toISOString(), local_metadata: {}, user_provided_metadata: {}, }, @@ -40,4 +41,36 @@ describe('Agent status service', () => { const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); expect(status).toEqual('online'); }); + + it('should return enrolling when agent is active but never checkin', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: true, + local_metadata: {}, + user_provided_metadata: {}, + }, + } as SavedObject); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('enrolling'); + }); + + it('should return unenrolling when agent is unenrolling', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: true, + last_checkin: new Date().toISOString(), + unenrollment_started_at: new Date().toISOString(), + local_metadata: {}, + user_provided_metadata: {}, + }, + } as SavedObject); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('unenrolling'); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 848e65b7931eb..7437321163749 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -99,7 +99,8 @@ exports[`tests loading base.yml: base.yml 1`] = ` "package": { "name": "nginx" }, - "managed_by": "ingest-manager" + "managed_by": "ingest-manager", + "managed": true } } `; @@ -203,7 +204,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "package": { "name": "coredns" }, - "managed_by": "ingest-manager" + "managed_by": "ingest-manager", + "managed": true } } `; @@ -1691,7 +1693,8 @@ exports[`tests loading system.yml: system.yml 1`] = ` "package": { "name": "system" }, - "managed_by": "ingest-manager" + "managed_by": "ingest-manager", + "managed": true } } `; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index e7867532ed176..77ad96952269f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -317,6 +317,7 @@ function getBaseTemplate( name: packageName, }, managed_by: 'ingest-manager', + managed: true, }, }; } 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 deleted file mode 100644 index ae6493d4716e8..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ /dev/null @@ -1,119 +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 { - SavedObject, - SavedObjectsBulkCreateObject, - SavedObjectsClientContract, -} from 'src/core/server'; -import * as Registry from '../../registry'; -import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; - -type SavedObjectToBe = Required & { type: AssetType }; -export type ArchiveAsset = Pick< - SavedObject, - 'id' | 'attributes' | 'migrationVersion' | 'references' -> & { - type: AssetType; -}; - -export async function getKibanaAsset(key: string) { - const buffer = Registry.getAsset(key); - - // cache values are buffers. convert to string / JSON - return JSON.parse(buffer.toString('utf8')); -} - -export function createSavedObjectKibanaAsset( - jsonAsset: ArchiveAsset, - pkgName: string -): SavedObjectToBe { - // convert that to an object - const asset = changeAssetIds(jsonAsset, pkgName); - - return { - type: asset.type, - id: asset.id, - attributes: asset.attributes, - references: asset.references || [], - migrationVersion: asset.migrationVersion || {}, - }; -} - -// modifies id property and the id property of references objects (not index-pattern) -// to be prepended with the package name to distinguish assets from Beats modules' assets -export const changeAssetIds = (asset: ArchiveAsset, pkgName: string): ArchiveAsset => { - const references = asset.references.map((ref) => { - if (ref.type === KibanaAssetType.indexPattern) return ref; - const id = getAssetId(ref.id, pkgName); - return { ...ref, id }; - }); - return { - ...asset, - id: getAssetId(asset.id, pkgName), - references, - }; -}; - -export const getAssetId = (id: string, pkgName: string) => { - return `${pkgName}-${id}`; -}; - -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case -export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - paths: string[]; -}) { - const { savedObjectsClient, paths, pkgName } = options; - - // Only install Kibana assets during package installation. - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths, pkgName }) - ); - - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array - return Promise.all(installationPromises).then((results) => results.flat()); -} - -async function installKibanaSavedObjects({ - savedObjectsClient, - assetType, - paths, - pkgName, -}: { - savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; - paths: string[]; - pkgName: string; -}) { - 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 toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset, pkgName)) - ); - - if (toBeSavedObjects.length === 0) { - return []; - } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; - } -} - -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/kibana/assets/tests/__snapshots__/install.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap deleted file mode 100644 index 638ed4b6118c9..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`a kibana asset id and its reference ids are appended with package name changeAssetIds output matches snapshot: dashboard.json 1`] = ` -{ - "attributes": { - "description": "Overview dashboard for the Nginx integration in Metrics", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": { - "filter": [], - "highlightAll": true, - "query": { - "language": "kuery", - "query": "" - }, - "version": true - } - }, - "optionsJSON": { - "darkTheme": false, - "hidePanelTitles": false, - "useMargins": true - }, - "panelsJSON": [ - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "1", - "w": 24, - "x": 24, - "y": 0 - }, - "panelIndex": "1", - "panelRefName": "panel_0", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "2", - "w": 24, - "x": 24, - "y": 12 - }, - "panelIndex": "2", - "panelRefName": "panel_1", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "3", - "w": 24, - "x": 0, - "y": 12 - }, - "panelIndex": "3", - "panelRefName": "panel_2", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "4", - "w": 24, - "x": 0, - "y": 0 - }, - "panelIndex": "4", - "panelRefName": "panel_3", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "5", - "w": 48, - "x": 0, - "y": 24 - }, - "panelIndex": "5", - "panelRefName": "panel_4", - "version": "7.3.0" - } - ], - "timeRestore": false, - "title": "[Metrics Nginx] Overview ECS", - "version": 1 - }, - "id": "nginx-023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", - "migrationVersion": { - "dashboard": "7.3.0" - }, - "references": [ - { - "id": "metrics-*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern" - }, - { - "id": "nginx-555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_0", - "type": "search" - }, - { - "id": "nginx-a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_1", - "type": "map" - }, - { - "id": "nginx-d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_2", - "type": "dashboard" - }, - { - "id": "nginx-47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "nginx-dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_4", - "type": "visualization" - } - ], - "type": "dashboard" -} -`; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json deleted file mode 100644 index e28a61ae5e18c..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "attributes": { - "description": "Overview dashboard for the Nginx integration in Metrics", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": { - "filter": [], - "highlightAll": true, - "query": { - "language": "kuery", - "query": "" - }, - "version": true - } - }, - "optionsJSON": { - "darkTheme": false, - "hidePanelTitles": false, - "useMargins": true - }, - "panelsJSON": [ - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "1", - "w": 24, - "x": 24, - "y": 0 - }, - "panelIndex": "1", - "panelRefName": "panel_0", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "2", - "w": 24, - "x": 24, - "y": 12 - }, - "panelIndex": "2", - "panelRefName": "panel_1", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "3", - "w": 24, - "x": 0, - "y": 12 - }, - "panelIndex": "3", - "panelRefName": "panel_2", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "4", - "w": 24, - "x": 0, - "y": 0 - }, - "panelIndex": "4", - "panelRefName": "panel_3", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "5", - "w": 48, - "x": 0, - "y": 24 - }, - "panelIndex": "5", - "panelRefName": "panel_4", - "version": "7.3.0" - } - ], - "timeRestore": false, - "title": "[Metrics Nginx] Overview ECS", - "version": 1 - }, - "id": "023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", - "migrationVersion": { - "dashboard": "7.3.0" - }, - "references": [ - { - "id": "metrics-*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern" - }, - { - "id": "555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_0", - "type": "search" - }, - { - "id": "a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_1", - "type": "map" - }, - { - "id": "d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_2", - "type": "dashboard" - }, - { - "id": "47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_4", - "type": "visualization" - } - ], - "type": "dashboard" -} \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts deleted file mode 100644 index f9bc4cdbf203f..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts +++ /dev/null @@ -1,35 +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 { readFileSync } from 'fs'; -import path from 'path'; -import { getAssetId, changeAssetIds } from '../install'; - -expect.addSnapshotSerializer({ - print(val) { - return JSON.stringify(val, null, 2); - }, - - test(val) { - return val; - }, -}); - -describe('a kibana asset id and its reference ids are appended with package name', () => { - const assetPath = path.join(__dirname, './dashboard.json'); - const kibanaAsset = JSON.parse(readFileSync(assetPath, 'utf-8')); - const pkgName = 'nginx'; - const modifiedAssetObject = changeAssetIds(kibanaAsset, pkgName); - - test('changeAssetIds output matches snapshot', () => { - expect(modifiedAssetObject).toMatchSnapshot(path.basename(assetPath)); - }); - - test('getAssetId', () => { - const id = '47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs'; - expect(getAssetId(id, pkgName)).toBe(`${pkgName}-${id}`); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts new file mode 100644 index 0000000000000..b623295c5e060 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.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 { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; +import { AssetType } from '../../../types'; +import * as Registry from '../registry'; + +type ArchiveAsset = Pick; +type SavedObjectToBe = Required & { type: AssetType }; + +export async function getObject(key: string) { + const buffer = Registry.getAsset(key); + + // cache values are buffers. convert to string / JSON + const json = buffer.toString('utf8'); + // convert that to an object + const asset: ArchiveAsset = JSON.parse(json); + + const { type, file } = Registry.pathParts(key); + const savedObject: SavedObjectToBe = { + type, + id: file.replace('.json', ''), + attributes: asset.attributes, + references: asset.references || [], + migrationVersion: asset.migrationVersion || {}, + }; + + return savedObject; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 57c4f77432455..4bb803dfaf912 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -23,7 +23,7 @@ export { SearchParams, } from './get'; -export { installPackage, ensureInstalledPackage } from './install'; +export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; 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 8f73bc9a02765..910283549abdf 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 @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, Installation, + KibanaAssetType, CallESAsCurrentUser, DefaultPackages, ElasticsearchAssetType, @@ -17,7 +18,7 @@ import { } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { getObject } from './get_objects'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; @@ -120,6 +121,7 @@ export async function installPackage(options: { installKibanaAssets({ savedObjectsClient, pkgName, + pkgVersion, paths, }), installPipelines(registryPackageInfo, paths, callCluster), @@ -183,6 +185,27 @@ export async function installPackage(options: { }); } +// TODO: make it an exhaustive list +// e.g. switch statement with cases for each enum key returning `never` for default case +export async function installKibanaAssets(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + pkgVersion: string; + paths: string[]; +}) { + const { savedObjectsClient, paths } = options; + + // Only install Kibana assets during package installation. + const kibanaAssetTypes = Object.values(KibanaAssetType); + const installationPromises = kibanaAssetTypes.map(async (assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + ); + + // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] + // call .flat to flatten into one dimensional array + return Promise.all(installationPromises).then((results) => results.flat()); +} + export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -217,3 +240,34 @@ export async function saveInstallationReferences(options: { return toSaveAssetRefs; } + +async function installKibanaSavedObjects({ + savedObjectsClient, + assetType, + paths, +}: { + savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; + paths: string[]; +}) { + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); + + if (toBeSavedObjects.length === 0) { + return []; + } else { + const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { + overwrite: true, + }); + const createdObjects = createResults.saved_objects; + const installed = createdObjects.map(toAssetReference); + return installed; + } +} + +function toAssetReference({ id, type }: SavedObject) { + const reference: AssetReference = { id, type: type as KibanaAssetType }; + + return reference; +} diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index a508c33e0347b..3e9209efcac04 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -32,6 +32,9 @@ export const PostAgentCheckinRequestSchema = { agentId: schema.string(), }), body: schema.object({ + status: schema.maybe( + schema.oneOf([schema.literal('online'), schema.literal('error'), schema.literal('degraded')]) + ), local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), events: schema.maybe(schema.arrayOf(NewAgentEventSchema)), }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 0d60bd588f710..4a79f30a17a05 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -324,8 +324,11 @@ describe('IndexPattern Data Panel', () => { }; } - async function testExistenceLoading(stateChanges?: unknown, propChanges?: unknown) { - const props = testProps(); + async function testExistenceLoading( + stateChanges?: unknown, + propChanges?: unknown, + props = testProps() + ) { const inst = mountWithIntl(); await act(async () => { @@ -536,6 +539,25 @@ describe('IndexPattern Data Panel', () => { expect(core.http.post).toHaveBeenCalledTimes(2); expect(overlapCount).toEqual(0); }); + + it("should default to empty dsl if query can't be parsed", async () => { + const props = { + ...testProps(), + query: { + language: 'kuery', + query: '@timestamp : NOT *', + }, + }; + await testExistenceLoading(undefined, undefined, props); + + expect((props.core.http.post as jest.Mock).mock.calls[0][1].body).toContain( + JSON.stringify({ + must_not: { + match_all: {}, + }, + }) + ); + }); }); describe('displaying field list', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index eb7940634d78e..91c068c2b4fab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public'; import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import { FieldItem } from './field_item'; @@ -74,6 +74,27 @@ const fieldTypeNames: Record = { ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }), }; +// Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by +// returning a query dsl object not matching anything +function buildSafeEsQuery( + indexPattern: IIndexPattern, + query: Query, + filters: Filter[], + queryConfig: EsQueryConfig +) { + try { + return esQuery.buildEsQuery(indexPattern, query, filters, queryConfig); + } catch (e) { + return { + bool: { + must_not: { + match_all: {}, + }, + }, + }; + } +} + export function IndexPatternDataPanel({ setState, state, @@ -106,7 +127,7 @@ export function IndexPatternDataPanel({ timeFieldName: indexPatterns[id].timeFieldName, })); - const dslQuery = esQuery.buildEsQuery( + const dslQuery = buildSafeEsQuery( indexPatterns[currentIndexPatternId] as IIndexPattern, query, filters, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 815725f4331a6..fabf9e9e9bfff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -198,10 +198,12 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { className={`lnsFieldItem__info ${infoIsOpen ? 'lnsFieldItem__info-isOpen' : ''}`} data-test-subj={`lnsFieldListPanelField-${field.name}`} onClick={() => { - togglePopover(); + if (exists) { + togglePopover(); + } }} onKeyPress={(event) => { - if (event.key === 'ENTER') { + if (exists && event.key === 'ENTER') { togglePopover(); } }} diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index a3bb32337f9f8..096f26eb22fe3 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index cb2a41dadbe9e..0a5656aa266bc 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 9370b77e29560..9da8bb958941b 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js index fb1ea026abaa0..77fb10c71091d 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js @@ -19,13 +19,13 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => { ), diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js index a1a46d8616554..24b51cccb4e45 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js @@ -82,13 +82,13 @@ export class RevertToBasic extends React.PureComponent { ), diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx index 65d40f1de2009..7220f377cf386 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -94,14 +94,14 @@ export class StartTrial extends Component {

), @@ -236,15 +236,15 @@ export class StartTrial extends Component { const description = ( ), diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 185de02d555b7..7f7a90eeba5a2 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -41,6 +41,8 @@ export const OPERATOR = 'included'; export const ENTRY_VALUE = 'some host name'; export const MATCH = 'match'; export const MATCH_ANY = 'match_any'; +export const MAX_IMPORT_PAYLOAD_BYTES = 40000000; +export const IMPORT_BUFFER_SIZE = 1000; export const LIST = 'list'; export const EXISTS = 'exists'; export const NESTED = 'nested'; diff --git a/x-pack/plugins/lists/server/config.mock.ts b/x-pack/plugins/lists/server/config.mock.ts new file mode 100644 index 0000000000000..3cf5040c73675 --- /dev/null +++ b/x-pack/plugins/lists/server/config.mock.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IMPORT_BUFFER_SIZE, + LIST_INDEX, + LIST_ITEM_INDEX, + MAX_IMPORT_PAYLOAD_BYTES, +} from '../common/constants.mock'; + +import { ConfigType } from './config'; + +export const getConfigMock = (): Partial => ({ + listIndex: LIST_INDEX, + listItemIndex: LIST_ITEM_INDEX, +}); + +export const getConfigMockDecoded = (): ConfigType => ({ + enabled: true, + importBufferSize: IMPORT_BUFFER_SIZE, + listIndex: LIST_INDEX, + listItemIndex: LIST_ITEM_INDEX, + maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES, +}); diff --git a/x-pack/plugins/lists/server/config.test.ts b/x-pack/plugins/lists/server/config.test.ts new file mode 100644 index 0000000000000..60501322dcfa2 --- /dev/null +++ b/x-pack/plugins/lists/server/config.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConfigSchema, ConfigType } from './config'; +import { getConfigMock, getConfigMockDecoded } from './config.mock'; + +describe('config_schema', () => { + test('it works with expected basic mock data set and defaults', () => { + expect(ConfigSchema.validate(getConfigMock())).toEqual(getConfigMockDecoded()); + }); + + test('it throws if given an invalid value', () => { + const mock: Partial & { madeUpValue: string } = { + madeUpValue: 'something', + ...getConfigMock(), + }; + expect(() => ConfigSchema.validate(mock)).toThrow( + '[madeUpValue]: definition for this key is missing' + ); + }); + + test('it throws if the "maxImportPayloadBytes" value is 0', () => { + const mock: ConfigType = { + ...getConfigMockDecoded(), + maxImportPayloadBytes: 0, + }; + expect(() => ConfigSchema.validate(mock)).toThrow( + '[maxImportPayloadBytes]: Value must be equal to or greater than [1].' + ); + }); + + test('it throws if the "maxImportPayloadBytes" value is less than 0', () => { + const mock: ConfigType = { + ...getConfigMockDecoded(), + maxImportPayloadBytes: -1, + }; + expect(() => ConfigSchema.validate(mock)).toThrow( + '[maxImportPayloadBytes]: Value must be equal to or greater than [1].' + ); + }); + + test('it throws if the "importBufferSize" value is 0', () => { + const mock: ConfigType = { + ...getConfigMockDecoded(), + importBufferSize: 0, + }; + expect(() => ConfigSchema.validate(mock)).toThrow( + '[importBufferSize]: Value must be equal to or greater than [1].' + ); + }); + + test('it throws if the "importBufferSize" value is less than 0', () => { + const mock: ConfigType = { + ...getConfigMockDecoded(), + importBufferSize: -1, + }; + expect(() => ConfigSchema.validate(mock)).toThrow( + '[importBufferSize]: Value must be equal to or greater than [1].' + ); + }); +}); diff --git a/x-pack/plugins/lists/server/config.ts b/x-pack/plugins/lists/server/config.ts index f2fa7e8801033..0fcc68419f8fe 100644 --- a/x-pack/plugins/lists/server/config.ts +++ b/x-pack/plugins/lists/server/config.ts @@ -8,8 +8,10 @@ import { TypeOf, schema } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + importBufferSize: schema.number({ defaultValue: 1000, min: 1 }), listIndex: schema.string({ defaultValue: '.lists' }), listItemIndex: schema.string({ defaultValue: '.items' }), + maxImportPayloadBytes: schema.number({ defaultValue: 40000000, min: 1 }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/lists/server/create_config.ts b/x-pack/plugins/lists/server/create_config.ts index 7e2e639ce7a35..e46c71798eb9f 100644 --- a/x-pack/plugins/lists/server/create_config.ts +++ b/x-pack/plugins/lists/server/create_config.ts @@ -12,12 +12,6 @@ import { ConfigType } from './config'; export const createConfig$ = ( context: PluginInitializerContext -): Observable< - Readonly<{ - enabled: boolean; - listIndex: string; - listItemIndex: string; - }> -> => { +): Observable> => { return context.config.create().pipe(map((config) => config)); }; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index cdd674a19ceb6..b4f2639f24923 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -48,7 +48,7 @@ export class ListPlugin core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); const router = core.http.createRouter(); - initRoutes(router); + initRoutes(router, config); return { getExceptionListClient: (savedObjectsClient, user): ExceptionListClient => { diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index d75199140ea8e..2e629d7516dd1 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -4,50 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Readable } from 'stream'; - import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { validate } from '../../common/siem_common_deps'; -import { importListItemQuerySchema, importListItemSchema, listSchema } from '../../common/schemas'; - -import { getListClient } from '.'; +import { importListItemQuerySchema, listSchema } from '../../common/schemas'; +import { ConfigType } from '../config'; -export interface HapiReadableStream extends Readable { - hapi: { - filename: string; - }; -} +import { createStreamFromBuffer } from './utils/create_stream_from_buffer'; -/** - * Special interface since we are streaming in a file through a reader - */ -export interface ImportListItemHapiFileSchema { - file: HapiReadableStream; -} +import { getListClient } from '.'; -export const importListItemRoute = (router: IRouter): void => { +export const importListItemRoute = (router: IRouter, config: ConfigType): void => { router.post( { options: { body: { - output: 'stream', + accepts: ['multipart/form-data'], + maxBytes: config.maxImportPayloadBytes, + parse: false, }, tags: ['access:lists'], }, path: `${LIST_ITEM_URL}/_import`, validate: { - body: buildRouteValidation( - importListItemSchema - ), + body: schema.buffer(), query: buildRouteValidation(importListItemQuerySchema), }, }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { + const stream = createStreamFromBuffer(request.body); const { deserializer, list_id: listId, serializer, type } = request.query; const lists = getListClient(context); if (listId != null) { @@ -63,7 +53,7 @@ export const importListItemRoute = (router: IRouter): void => { listId, meta: undefined, serializer: list.serializer, - stream: request.body.file, + stream, type: list.type, }); @@ -74,26 +64,21 @@ export const importListItemRoute = (router: IRouter): void => { return response.ok({ body: validated ?? {} }); } } else if (type != null) { - const { filename } = request.body.file.hapi; - // TODO: Should we prevent the same file from being uploaded multiple times? - const list = await lists.createListIfItDoesNotExist({ - description: `File uploaded from file system of ${filename}`, + const importedList = await lists.importListItemsToStream({ deserializer, - id: filename, + listId: undefined, meta: undefined, - name: filename, serializer, + stream, type, }); - await lists.importListItemsToStream({ - deserializer: list.deserializer, - listId: list.id, - meta: undefined, - serializer: list.serializer, - stream: request.body.file, - type: list.type, - }); - const [validated, errors] = validate(list, listSchema); + if (importedList == null) { + return siemResponse.error({ + body: 'Unable to parse a valid fileName during import', + statusCode: 400, + }); + } + const [validated, errors] = validate(importedList, listSchema); if (errors != null) { return siemResponse.error({ body: errors, statusCode: 500 }); } else { diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index e74fa471734b0..ffd8afd54913f 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -6,6 +6,8 @@ import { IRouter } from 'kibana/server'; +import { ConfigType } from '../config'; + import { createExceptionListItemRoute, createExceptionListRoute, @@ -36,7 +38,7 @@ import { updateListRoute, } from '.'; -export const initRoutes = (router: IRouter): void => { +export const initRoutes = (router: IRouter, config: ConfigType): void => { // lists createListRoute(router); readListRoute(router); @@ -52,7 +54,7 @@ export const initRoutes = (router: IRouter): void => { deleteListItemRoute(router); patchListItemRoute(router); exportListItemRoute(router); - importListItemRoute(router); + importListItemRoute(router, config); findListItemRoute(router); // indexes of lists diff --git a/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts b/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts new file mode 100644 index 0000000000000..3dcf03617bcbc --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Readable } from 'stream'; + +export const createStreamFromBuffer = (buffer: Buffer): Readable => { + const stream = new Readable(); + stream.push(buffer); + stream.push(null); + return stream; +}; diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts index a283269271bd0..ad1511e28f80a 100644 --- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -4,15 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IMPORT_BUFFER_SIZE } from '../../../common/constants.mock'; + import { BufferLines } from './buffer_lines'; import { TestReadable } from './test_readable.mock'; describe('buffer_lines', () => { + test('it will throw if given a buffer size of zero', () => { + expect(() => { + new BufferLines({ bufferSize: 0, input: new TestReadable() }); + }).toThrow('bufferSize must be greater than zero'); + }); + + test('it will throw if given a buffer size of -1', () => { + expect(() => { + new BufferLines({ bufferSize: -1, input: new TestReadable() }); + }).toThrow('bufferSize must be greater than zero'); + }); + test('it can read a single line', (done) => { const input = new TestReadable(); input.push('line one\n'); input.push(null); - const bufferedLine = new BufferLines({ input }); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one']); + done(); + }); + }); + + test('it can read a single line using a buffer size of 1', (done) => { + const input = new TestReadable(); + input.push('line one\n'); + input.push(null); + const bufferedLine = new BufferLines({ bufferSize: 1, input }); let linesToTest: string[] = []; bufferedLine.on('lines', (lines: string[]) => { linesToTest = [...linesToTest, ...lines]; @@ -28,7 +57,23 @@ describe('buffer_lines', () => { input.push('line one\n'); input.push('line two\n'); input.push(null); - const bufferedLine = new BufferLines({ input }); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one', 'line two']); + done(); + }); + }); + + test('it can read two lines using a buffer size of 1', (done) => { + const input = new TestReadable(); + input.push('line one\n'); + input.push('line two\n'); + input.push(null); + const bufferedLine = new BufferLines({ bufferSize: 1, input }); let linesToTest: string[] = []; bufferedLine.on('lines', (lines: string[]) => { linesToTest = [...linesToTest, ...lines]; @@ -44,7 +89,7 @@ describe('buffer_lines', () => { input.push('line one\n'); input.push('line one\n'); input.push(null); - const bufferedLine = new BufferLines({ input }); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); let linesToTest: string[] = []; bufferedLine.on('lines', (lines: string[]) => { linesToTest = [...linesToTest, ...lines]; @@ -58,7 +103,7 @@ describe('buffer_lines', () => { test('it can close out without writing any lines', (done) => { const input = new TestReadable(); input.push(null); - const bufferedLine = new BufferLines({ input }); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); let linesToTest: string[] = []; bufferedLine.on('lines', (lines: string[]) => { linesToTest = [...linesToTest, ...lines]; @@ -71,7 +116,7 @@ describe('buffer_lines', () => { test('it can read 200 lines', (done) => { const input = new TestReadable(); - const bufferedLine = new BufferLines({ input }); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); let linesToTest: string[] = []; const size200: string[] = new Array(200).fill(null).map((_, index) => `${index}\n`); size200.forEach((element) => input.push(element)); @@ -84,4 +129,66 @@ describe('buffer_lines', () => { done(); }); }); + + test('it can read an example multi-part message', (done) => { + const input = new TestReadable(); + input.push('--boundary\n'); + input.push('Content-type: text/plain\n'); + input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n'); + input.push('\n'); + input.push('127.0.0.1\n'); + input.push('127.0.0.2\n'); + input.push('127.0.0.3\n'); + input.push('\n'); + input.push('--boundary--\n'); + input.push(null); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['127.0.0.1', '127.0.0.2', '127.0.0.3']); + done(); + }); + }); + + test('it can read an empty multi-part message', (done) => { + const input = new TestReadable(); + input.push('--boundary\n'); + input.push('Content-type: text/plain\n'); + input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n'); + input.push('\n'); + input.push('\n'); + input.push('--boundary--\n'); + input.push(null); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual([]); + done(); + }); + }); + + test('it can read a fileName from a multipart message', (done) => { + const input = new TestReadable(); + input.push('--boundary\n'); + input.push('Content-type: text/plain\n'); + input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n'); + input.push('\n'); + input.push('--boundary--\n'); + input.push(null); + const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input }); + let fileNameToTest: string; + bufferedLine.on('fileName', (fileName: string) => { + fileNameToTest = fileName; + }); + bufferedLine.on('close', () => { + expect(fileNameToTest).toEqual('filename.text'); + done(); + }); + }); }); diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.ts index 4ff84268f5e0c..dc257eadb7438 100644 --- a/x-pack/plugins/lists/server/services/items/buffer_lines.ts +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.ts @@ -7,18 +7,50 @@ import readLine from 'readline'; import { Readable } from 'stream'; -const BUFFER_SIZE = 100; - export class BufferLines extends Readable { private set = new Set(); - constructor({ input }: { input: NodeJS.ReadableStream }) { + private boundary: string | null = null; + private readableText: boolean = false; + private paused: boolean = false; + private bufferSize: number; + constructor({ input, bufferSize }: { input: NodeJS.ReadableStream; bufferSize: number }) { super({ encoding: 'utf-8' }); + if (bufferSize <= 0) { + throw new RangeError('bufferSize must be greater than zero'); + } + this.bufferSize = bufferSize; + const readline = readLine.createInterface({ input, }); + // We are parsing multipart/form-data involving boundaries as fast as we can to get + // * The filename if it exists and emit it + // * The actual content within the multipart/form-data readline.on('line', (line) => { - this.push(line); + if (this.boundary == null && line.startsWith('--')) { + this.boundary = `${line}--`; + } else if (this.boundary != null && !this.readableText && line.trim() !== '') { + if (line.startsWith('Content-Disposition')) { + const fileNameMatch = RegExp('filename="(?.+)"'); + const matches = fileNameMatch.exec(line); + if (matches?.groups?.fileName != null) { + this.emit('fileName', matches.groups.fileName); + } + } + } else if (this.boundary != null && !this.readableText && line.trim() === '') { + // we are ready to be readable text now for parsing + this.readableText = true; + } else if (this.readableText && line.trim() === '') { + // skip and do nothing as this is either a empty line or an upcoming end is about to happen + } else if (this.boundary != null && this.readableText && line === this.boundary) { + // we are at the end of the stream + this.boundary = null; + this.readableText = false; + } else { + // we have actual content to push + this.push(line); + } }); readline.on('close', () => { @@ -26,23 +58,54 @@ export class BufferLines extends Readable { }); } - public _read(): void { - // No operation but this is required to be implemented + public _read(): void {} + + public pause(): this { + this.paused = true; + return this; } - public push(line: string | null): boolean { - if (line == null) { - this.emit('lines', Array.from(this.set)); - this.set.clear(); - this.emit('close'); - return true; + public resume(): this { + this.paused = false; + return this; + } + + private emptyBuffer(): void { + const arrayFromSet = Array.from(this.set); + if (arrayFromSet.length === 0) { + this.emit('lines', []); } else { + while (arrayFromSet.length) { + const spliced = arrayFromSet.splice(0, this.bufferSize); + this.emit('lines', spliced); + } + } + this.set.clear(); + } + + public push(line: string | null): boolean { + if (line != null) { this.set.add(line); - if (this.set.size > BUFFER_SIZE) { - this.emit('lines', Array.from(this.set)); - this.set.clear(); + if (this.paused) { + return false; + } else { + if (this.set.size > this.bufferSize) { + this.emptyBuffer(); + } return true; + } + } else { + if (this.paused) { + // If we paused but have buffered all of the available data + // we should do wait for 10(ms) and check again if we are paused + // or not. + setTimeout(() => { + this.push(line); + }, 10); + return false; } else { + this.emptyBuffer(); + this.emit('close'); return true; } } diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index 7fbdc900fe2a4..76bd47d217107 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -36,6 +36,7 @@ describe('crete_list_item', () => { body, id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('index', expected); }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index 333f34946828a..aa17fc00b25c6 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -71,6 +71,7 @@ export const createListItem = async ({ body, id, index: listItemIndex, + refresh: 'wait_for', }); return { diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index 4ab1bfb856846..b2cc0da669e42 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -33,6 +33,7 @@ describe('crete_list_item_bulk', () => { secondRecord, ], index: LIST_ITEM_INDEX, + refresh: 'wait_for', }); }); @@ -70,6 +71,7 @@ describe('crete_list_item_bulk', () => { }, ], index: '.items', + refresh: 'wait_for', }); }); }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 447c0f6bf95cc..91e9587aa676a 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -80,9 +80,13 @@ export const createListItemsBulk = async ({ }, [] ); - - await callCluster('bulk', { - body, - index: listItemIndex, - }); + try { + await callCluster('bulk', { + body, + index: listItemIndex, + refresh: 'wait_for', + }); + } catch (error) { + // TODO: Log out the error with return values from the bulk insert into another index or saved object + } }; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts index ea338d9dd3791..b14bddb1268f8 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -47,6 +47,7 @@ describe('delete_list_item', () => { const deleteQuery = { id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('delete', deleteQuery); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index b006aed6f6dde..baeced4b09995 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -28,6 +28,7 @@ export const deleteListItem = async ({ await callCluster('delete', { id, index: listItemIndex, + refresh: 'wait_for', }); } return listItem; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index bf1608334ef24..f658a51730d97 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -52,6 +52,7 @@ describe('delete_list_item_by_value', () => { }, }, index: '.items', + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index 3551cb75dc5bc..880402fca1bfa 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -48,6 +48,7 @@ export const deleteListItemByValue = async ({ }, }, index: listItemIndex, + refresh: 'wait_for', }); return listItems; }; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 24cd11cbb65e4..eb20f1cfe3b30 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -62,6 +62,7 @@ export const updateListItem = async ({ }, id: listItem.id, index: listItemIndex, + refresh: 'wait_for', }); return { created_at: listItem.created_at, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts index b7e30e0a1c308..d868351fc4b33 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts @@ -5,14 +5,24 @@ */ import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { ImportListItemsToStreamOptions, WriteBufferToItemsOptions } from '../items'; -import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from '../../../common/constants.mock'; +import { + LIST_ID, + LIST_INDEX, + LIST_ITEM_INDEX, + META, + TYPE, + USER, +} from '../../../common/constants.mock'; +import { getConfigMockDecoded } from '../../config.mock'; import { TestReadable } from './test_readable.mock'; export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ callCluster: getCallClusterMock(), + config: getConfigMockDecoded(), deserializer: undefined, listId: LIST_ID, + listIndex: LIST_INDEX, listItemIndex: LIST_ITEM_INDEX, meta: META, serializer: undefined, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 31b2b74c88431..2bffe338e9075 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -8,20 +8,26 @@ import { Readable } from 'stream'; import { LegacyAPICaller } from 'kibana/server'; +import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { DeserializerOrUndefined, + ListIdOrUndefined, + ListSchema, MetaOrUndefined, SerializerOrUndefined, Type, } from '../../../common/schemas'; +import { ConfigType } from '../../config'; import { BufferLines } from './buffer_lines'; import { createListItemsBulk } from './create_list_items_bulk'; export interface ImportListItemsToStreamOptions { + listId: ListIdOrUndefined; + config: ConfigType; + listIndex: string; deserializer: DeserializerOrUndefined; serializer: SerializerOrUndefined; - listId: string; stream: Readable; callCluster: LegacyAPICaller; listItemIndex: string; @@ -31,34 +37,72 @@ export interface ImportListItemsToStreamOptions { } export const importListItemsToStream = ({ + config, deserializer, serializer, listId, stream, callCluster, listItemIndex, + listIndex, type, user, meta, -}: ImportListItemsToStreamOptions): Promise => { - return new Promise((resolve) => { - const readBuffer = new BufferLines({ input: stream }); +}: ImportListItemsToStreamOptions): Promise => { + return new Promise((resolve) => { + const readBuffer = new BufferLines({ bufferSize: config.importBufferSize, input: stream }); + let fileName: string | undefined; + let list: ListSchema | null = null; + readBuffer.on('fileName', async (fileNameEmitted: string) => { + readBuffer.pause(); + fileName = fileNameEmitted; + if (listId == null) { + list = await createListIfItDoesNotExist({ + callCluster, + description: `File uploaded from file system of ${fileNameEmitted}`, + deserializer, + id: fileNameEmitted, + listIndex, + meta, + name: fileNameEmitted, + serializer, + type, + user, + }); + } + readBuffer.resume(); + }); + readBuffer.on('lines', async (lines: string[]) => { - await writeBufferToItems({ - buffer: lines, - callCluster, - deserializer, - listId, - listItemIndex, - meta, - serializer, - type, - user, - }); + if (listId != null) { + await writeBufferToItems({ + buffer: lines, + callCluster, + deserializer, + listId, + listItemIndex, + meta, + serializer, + type, + user, + }); + } else if (fileName != null) { + await writeBufferToItems({ + buffer: lines, + callCluster, + deserializer, + listId: fileName, + listItemIndex, + meta, + serializer, + type, + user, + }); + } }); readBuffer.on('close', () => { - resolve(); + resolve(list); }); }); }; diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index 43af08bcaf7ff..e328df710ebe1 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -52,6 +52,7 @@ describe('crete_list', () => { body, id: LIST_ID, index: LIST_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('index', expected); }); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 3925fa5f0170c..3d396cf4d5af9 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -67,6 +67,7 @@ export const createList = async ({ body, id, index: listIndex, + refresh: 'wait_for', }); return { id: response._id, diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts new file mode 100644 index 0000000000000..84f5ac0308191 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LegacyAPICaller } from 'kibana/server'; + +import { + Description, + DeserializerOrUndefined, + Id, + ListSchema, + MetaOrUndefined, + Name, + SerializerOrUndefined, + Type, +} from '../../../common/schemas'; + +import { getList } from './get_list'; +import { createList } from './create_list'; + +export interface CreateListIfItDoesNotExistOptions { + id: Id; + type: Type; + name: Name; + deserializer: DeserializerOrUndefined; + serializer: SerializerOrUndefined; + description: Description; + callCluster: LegacyAPICaller; + listIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createListIfItDoesNotExist = async ({ + id, + name, + type, + description, + deserializer, + callCluster, + listIndex, + user, + meta, + serializer, + dateNow, + tieBreaker, +}: CreateListIfItDoesNotExistOptions): Promise => { + const list = await getList({ callCluster, id, listIndex }); + if (list == null) { + return createList({ + callCluster, + dateNow, + description, + deserializer, + id, + listIndex, + meta, + name, + serializer, + tieBreaker, + type, + user, + }); + } else { + return list; + } +}; diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index b9f1ec4d400be..029b6226a7375 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -47,6 +47,7 @@ describe('delete_list', () => { const deleteByQuery = { body: { query: { term: { list_id: LIST_ID } } }, index: LIST_ITEM_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); @@ -59,6 +60,7 @@ describe('delete_list', () => { const deleteQuery = { id: LIST_ID, index: LIST_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); }); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 64359b7273274..152048ca9cac6 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -36,11 +36,13 @@ export const deleteList = async ({ }, }, index: listItemIndex, + refresh: 'wait_for', }); await callCluster('delete', { id, index: listIndex, + refresh: 'wait_for', }); return list; } diff --git a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts index 43a01a3ca62dc..e5036d561ddc6 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts @@ -9,7 +9,12 @@ import { getFoundListSchemaMock } from '../../../common/schemas/response/found_l import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; -import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; +import { + IMPORT_BUFFER_SIZE, + LIST_INDEX, + LIST_ITEM_INDEX, + MAX_IMPORT_PAYLOAD_BYTES, +} from '../../../common/constants.mock'; import { ListClient } from './list_client'; @@ -59,8 +64,10 @@ export const getListClientMock = (): ListClient => { callCluster: getCallClusterMock(), config: { enabled: true, + importBufferSize: IMPORT_BUFFER_SIZE, listIndex: LIST_INDEX, listItemIndex: LIST_ITEM_INDEX, + maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES, }, spaceId: 'default', user: 'elastic', diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index be9da1a1c69f5..4acc2e7092491 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -70,6 +70,7 @@ import { UpdateListItemOptions, UpdateListOptions, } from './list_client_types'; +import { createListIfItDoesNotExist } from './create_list_if_it_does_not_exist'; export class ListClient { private readonly spaceId: string; @@ -140,12 +141,20 @@ export class ListClient { type, meta, }: CreateListIfItDoesNotExistOptions): Promise => { - const list = await this.getList({ id }); - if (list == null) { - return this.createList({ description, deserializer, id, meta, name, serializer, type }); - } else { - return list; - } + const { callCluster, user } = this; + const listIndex = this.getListIndex(); + return createListIfItDoesNotExist({ + callCluster, + description, + deserializer, + id, + listIndex, + meta, + name, + serializer, + type, + user, + }); }; public getListIndexExists = async (): Promise => { @@ -325,13 +334,16 @@ export class ListClient { listId, stream, meta, - }: ImportListItemsToStreamOptions): Promise => { - const { callCluster, user } = this; + }: ImportListItemsToStreamOptions): Promise => { + const { callCluster, user, config } = this; const listItemIndex = this.getListItemIndex(); + const listIndex = this.getListIndex(); return importListItemsToStream({ callCluster, + config, deserializer, listId, + listIndex, listItemIndex, meta, serializer, diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index 26e147a6fa130..68a018fa2fc16 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -16,6 +16,7 @@ import { Id, IdOrUndefined, ListId, + ListIdOrUndefined, MetaOrUndefined, Name, NameOrUndefined, @@ -86,9 +87,9 @@ export interface ExportListItemsToStreamOptions { } export interface ImportListItemsToStreamOptions { + listId: ListIdOrUndefined; deserializer: DeserializerOrUndefined; serializer: SerializerOrUndefined; - listId: string; type: Type; stream: Readable; meta: MetaOrUndefined; diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index c7cc30aaae908..f84ca787eaa7c 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -55,6 +55,7 @@ export const updateList = async ({ body: { doc }, id, index: listIndex, + refresh: 'wait_for', }); return { created_at: list.created_at, diff --git a/x-pack/plugins/ml/common/constants/field_types.ts b/x-pack/plugins/ml/common/constants/field_types.ts index 9402e4c20e46f..93641fd45c499 100644 --- a/x-pack/plugins/ml/common/constants/field_types.ts +++ b/x-pack/plugins/ml/common/constants/field_types.ts @@ -17,3 +17,6 @@ export enum ML_JOB_FIELD_TYPES { export const MLCATEGORY = 'mlcategory'; export const DOC_COUNT = 'doc_count'; + +// List of system fields we don't want to display. +export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts index 751413bb6485a..d5c532234fd2b 100644 --- a/x-pack/plugins/ml/common/constants/new_job.ts +++ b/x-pack/plugins/ml/common/constants/new_job.ts @@ -17,6 +17,7 @@ export enum CREATED_BY_LABEL { MULTI_METRIC = 'multi-metric-wizard', POPULATION = 'population-wizard', CATEGORIZATION = 'categorization-wizard', + APM_TRANSACTION = 'ml-module-apm-transaction', } export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts index e2c4f1bae1a10..744f9c4d759dd 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts @@ -65,7 +65,7 @@ export interface Detector { function: string; over_field_name?: string; partition_field_name?: string; - use_null?: string; + use_null?: boolean; custom_rules?: CustomRule[]; } export interface AnalysisLimits { @@ -80,7 +80,7 @@ export interface DataDescription { } export interface ModelPlotConfig { - enabled: boolean; + enabled?: boolean; annotations_enabled?: boolean; terms?: string; } diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx index 83e7b82986cf8..d71a180cd2206 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx @@ -11,13 +11,17 @@ import { mount } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; +import { UrlStateProvider } from '../../../util/url_state'; + import { SelectInterval } from './select_interval'; describe('SelectInterval', () => { test('creates correct initial selected value', () => { const wrapper = mount( - + + + ); const select = wrapper.find(EuiSelect); @@ -29,7 +33,9 @@ describe('SelectInterval', () => { test('currently selected value is updated correctly on click', (done) => { const wrapper = mount( - + + + ); const select = wrapper.find(EuiSelect).first(); diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx index 484a0c395f3f8..cb4f80bfe6809 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx @@ -11,13 +11,17 @@ import { mount } from 'enzyme'; import { EuiSuperSelect } from '@elastic/eui'; +import { UrlStateProvider } from '../../../util/url_state'; + import { SelectSeverity } from './select_severity'; describe('SelectSeverity', () => { test('creates correct severity options and initial selected value', () => { const wrapper = mount( - + + + ); const select = wrapper.find(EuiSuperSelect); @@ -65,7 +69,9 @@ describe('SelectSeverity', () => { test('state for currently selected value is updated correctly on click', (done) => { const wrapper = mount( - + + + ); diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 859d649416267..3a4875fa243fd 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -60,7 +60,7 @@ function getTabs(disableLinks: boolean): Tab[] { name: i18n.translate('xpack.ml.navMenu.settingsTabLinkText', { defaultMessage: 'Settings', }), - disabled: false, + disabled: disableLinks, }, ]; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index aa637f71db1cc..618ea5184007d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -121,16 +121,24 @@ export interface DfAnalyticsExplainResponse { } export interface Eval { - meanSquaredError: number | string; + mse: number | string; + msle: number | string; + huber: number | string; rSquared: number | string; error: null | string; } export interface RegressionEvaluateResponse { regression: { + huber: { + value: number; + }; mse: { value: number; }; + msle: { + value: number; + }; r_squared: { value: number; }; @@ -414,19 +422,37 @@ export const useRefreshAnalyticsList = ( const DEFAULT_SIG_FIGS = 3; -export function getValuesFromResponse(response: RegressionEvaluateResponse) { - let meanSquaredError = response?.regression?.mse?.value; +interface RegressionEvaluateExtractedResponse { + mse: number | string; + msle: number | string; + huber: number | string; + r_squared: number | string; +} - if (meanSquaredError) { - meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS)); - } +export const EMPTY_STAT = '--'; - let rSquared = response?.regression?.r_squared?.value; - if (rSquared) { - rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS)); +export function getValuesFromResponse(response: RegressionEvaluateResponse) { + const results: RegressionEvaluateExtractedResponse = { + mse: EMPTY_STAT, + msle: EMPTY_STAT, + huber: EMPTY_STAT, + r_squared: EMPTY_STAT, + }; + + if (response?.regression) { + for (const statType in response.regression) { + if (response.regression.hasOwnProperty(statType)) { + let currentStatValue = + response.regression[statType as keyof RegressionEvaluateResponse['regression']]?.value; + if (currentStatValue) { + currentStatValue = Number(currentStatValue.toPrecision(DEFAULT_SIG_FIGS)); + } + results[statType as keyof RegressionEvaluateExtractedResponse] = currentStatValue; + } + } } - return { meanSquaredError, rSquared }; + return results; } interface ResultsSearchBoolQuery { bool: Dictionary; @@ -490,13 +516,22 @@ export function getEvalQueryBody({ return query; } +export enum REGRESSION_STATS { + MSE = 'mse', + MSLE = 'msle', + R_SQUARED = 'rSquared', + HUBER = 'huber', +} + interface EvaluateMetrics { classification: { multiclass_confusion_matrix: object; }; regression: { r_squared: object; - mean_squared_error: object; + mse: object; + msle: object; + huber: object; }; } @@ -541,7 +576,9 @@ export const loadEvalData = async ({ }, regression: { r_squared: {}, - mean_squared_error: {}, + mse: {}, + msle: {}, + huber: {}, }, }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index b83dd2e4329e0..9dae54b6537b3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -71,12 +71,8 @@ export const ConfigurationStepForm: FC = ({ EuiComboBoxOptionOption[] >([]); const [includesTableItems, setIncludesTableItems] = useState([]); - const [maxDistinctValuesError, setMaxDistinctValuesError] = useState( - undefined - ); - const [unsupportedFieldsError, setUnsupportedFieldsError] = useState( - undefined - ); + const [maxDistinctValuesError, setMaxDistinctValuesError] = useState(); + const [unsupportedFieldsError, setUnsupportedFieldsError] = useState(); const { setEstimatedModelMemoryLimit, setFormState } = actions; const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts index bf3ab01549139..0935ed15a1a4a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts @@ -12,9 +12,6 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']); -// List of system fields we want to ignore for the numeric field check. -export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; - // Regression supports numeric fields. Classification supports categorical, numeric, and boolean. export const shouldAddAsDepVarOption = (field: Field, jobType: AnalyticsJobType) => { if (field.id === EVENT_RATE_FIELD_ID) return false; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx index 0a4ba67831818..88c89df86b29a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -11,8 +11,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; +import { OMIT_FIELDS } from '../../../../../../../common/constants/field_types'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields'; -import { OMIT_FIELDS, CATEGORICAL_TYPES } from './form_options_validation'; +import { CATEGORICAL_TYPES } from './form_options_validation'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx index a4d86b48006e8..8a41eb4b8a865 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx @@ -26,7 +26,7 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ state, }) => { const { form, isJobCreated } = state; - const { description, jobId, destinationIndex } = form; + const { description, jobId, destinationIndex, resultsField } = form; const detailsFirstCol: ListItems[] = [ { @@ -37,6 +37,19 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ }, ]; + if ( + resultsField !== undefined && + typeof resultsField === 'string' && + resultsField.trim() !== '' + ) { + detailsFirstCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.resultsField', { + defaultMessage: 'Results field', + }), + description: resultsField, + }); + } + const detailsSecondCol: ListItems[] = [ { title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', { 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 d846ae95c2c7e..168d5e31f57c3 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 @@ -47,6 +47,7 @@ export const DetailsStepForm: FC = ({ jobIdExists, jobIdInvalidMaxLength, jobIdValid, + resultsField, } = form; const forceInput = useRef(null); @@ -195,6 +196,22 @@ export const DetailsStepForm: FC = ({ data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput" /> + + setFormState({ resultsField: e.target.value })} + data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput" + /> + = ({ jobConfig, jobStatus, searchQuery }) => { const { @@ -82,18 +94,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) genErrorEval.eval && isRegressionEvaluateResponse(genErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingGeneralization(false); } else { setIsLoadingGeneralization(false); setGeneralizationEval({ - meanSquaredError: '--', - rSquared: '--', + ...EMPTY_STATS, error: genErrorEval.error, }); } @@ -118,18 +131,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) trainingErrorEval.eval && isRegressionEvaluateResponse(trainingErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingTraining(false); } else { setIsLoadingTraining(false); setTrainingEval({ - meanSquaredError: '--', - rSquared: '--', + ...EMPTY_STATS, error: trainingErrorEval.error, }); } @@ -274,22 +288,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - + + {/* First row stats */} - + + + + + + + + + {/* Second row stats */} - + + + + + + + + @@ -331,22 +371,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - + + {/* First row stats */} - + + + + + + + + + {/* Second row stats */} - + + + + + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx index 1b4461b2bb075..114ec75efb2e7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx @@ -6,58 +6,99 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { REGRESSION_STATS } from '../../../../common/analytics'; interface Props { isLoading: boolean; title: number | string; - isMSE: boolean; + statType: REGRESSION_STATS; dataTestSubj: string; } -const meanSquaredErrorText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', - { - defaultMessage: 'Mean squared error', - } -); -const rSquaredText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', - { - defaultMessage: 'R squared', - } -); -const meanSquaredErrorTooltipContent = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent', - { - defaultMessage: - 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.', - } -); -const rSquaredTooltipContent = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent', - { - defaultMessage: - 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.', - } -); +const statDescriptions = { + [REGRESSION_STATS.MSE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', + { + defaultMessage: 'Mean squared error', + } + ), + [REGRESSION_STATS.MSLE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.msleText', + { + defaultMessage: 'Mean squared logarithmic error', + } + ), + [REGRESSION_STATS.R_SQUARED]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', + { + defaultMessage: 'R squared', + } + ), + [REGRESSION_STATS.HUBER]: ( + + {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.huberLinkText', { + defaultMessage: 'Pseudo Huber loss function', + })} + + ), + }} + /> + ), +}; + +const tooltipContent = { + [REGRESSION_STATS.MSE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent', + { + defaultMessage: + 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.', + } + ), + [REGRESSION_STATS.MSLE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.msleTooltipContent', + { + defaultMessage: + 'Average squared difference between the logarithm of the predicted values and the logarithm of the actual (ground truth) value', + } + ), + [REGRESSION_STATS.R_SQUARED]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent', + { + defaultMessage: + 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.', + } + ), +}; -export const EvaluateStat: FC = ({ isLoading, isMSE, title, dataTestSubj }) => ( +export const EvaluateStat: FC = ({ isLoading, statType, title, dataTestSubj }) => ( - + {statType !== REGRESSION_STATS.HUBER && ( + + )} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts index 006cccf3b4610..9db32e298691e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts @@ -131,7 +131,7 @@ describe('Analytics job clone action', () => { }, analyzed_fields: { includes: [], - excludes: [], + excludes: ['excluded_field'], }, model_memory_limit: '350mb', allow_lazy_start: false, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index f8b6fdfbe2119..280ec544c1e5e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -247,6 +247,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, + formKey: 'resultsField', defaultValue: DEFAULT_RESULTS_FIELD, }, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 405231aef5774..4080f6cd7a77e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -39,6 +39,7 @@ import { import { getAnalyticsFactory } from '../../services/analytics_service'; import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; +import { stringMatch } from '../../../../../util/string_utils'; import { ProgressBar, mlInMemoryTableFactory, @@ -65,14 +66,6 @@ function getItemIdToExpandedRowMap( }, {} as ItemIdToExpandedRowMap); } -function stringMatch(str: string | undefined, substr: any) { - return ( - typeof str === 'string' && - typeof substr === 'string' && - (str.toLowerCase().match(substr.toLowerCase()) === null) === false - ); -} - const MlInMemoryTable = mlInMemoryTableFactory(); interface Props { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 4d029ff1d9546..5276fedff0fde 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -29,6 +29,7 @@ import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './c import { isRegressionAnalysis, ANALYSIS_CONFIG_TYPE, + REGRESSION_STATS, isRegressionEvaluateResponse, } from '../../../../common/analytics'; import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; @@ -44,7 +45,7 @@ function getItemDescription(value: any) { interface LoadedStatProps { isLoading: boolean; evalData: Eval; - resultProperty: 'meanSquaredError' | 'rSquared'; + resultProperty: REGRESSION_STATS; } const LoadedStat: FC = ({ isLoading, evalData, resultProperty }) => { @@ -61,7 +62,7 @@ interface Props { item: DataFrameAnalyticsListRow; } -const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; +const defaultEval: Eval = { mse: '', msle: '', huber: '', rSquared: '', error: null }; export const ExpandedRow: FC = ({ item }) => { const [trainingEval, setTrainingEval] = useState(defaultEval); @@ -94,17 +95,21 @@ export const ExpandedRow: FC = ({ item }) => { genErrorEval.eval && isRegressionEvaluateResponse(genErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingGeneralization(false); } else { setIsLoadingGeneralization(false); setGeneralizationEval({ - meanSquaredError: '', + mse: '', + msle: '', + huber: '', rSquared: '', error: genErrorEval.error, }); @@ -124,17 +129,21 @@ export const ExpandedRow: FC = ({ item }) => { trainingErrorEval.eval && isRegressionEvaluateResponse(trainingErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingTraining(false); } else { setIsLoadingTraining(false); setTrainingEval({ - meanSquaredError: '', + mse: '', + msle: '', + huber: '', rSquared: '', error: genErrorEval.error, }); @@ -221,7 +230,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'generalization mean squared logarithmic error', + description: ( + ), }, @@ -231,7 +250,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'generalization pseudo huber loss function', + description: ( + ), }, @@ -241,7 +270,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'training mean squared logarithmic error', + description: ( + ), }, @@ -251,7 +290,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'training pseudo huber loss function', + description: ( + ), } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index e75d938116991..cb46a88fa3b21 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -30,17 +30,21 @@ export const useActions = ( actions: EuiTableActionsColumnType['actions']; modals: JSX.Element | null; } => { - const deleteAction = useDeleteAction(); - const editAction = useEditAction(); - const startAction = useStartAction(); - let modals: JSX.Element | null = null; const actions: EuiTableActionsColumnType['actions'] = [ getViewAction(isManagementTable), ]; + // isManagementTable will be the same for the lifecycle of the component + // Disabling lint error to fix console error in management list due to action hooks using deps not initialized in management if (isManagementTable === false) { + /* eslint-disable react-hooks/rules-of-hooks */ + const deleteAction = useDeleteAction(); + const editAction = useEditAction(); + const startAction = useStartAction(); + /* eslint-disable react-hooks/rules-of-hooks */ + modals = ( <> {startAction.isModalVisible && } 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 81d35679443b8..b344e44c97d59 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 @@ -144,6 +144,11 @@ export const validateAdvancedEditor = (state: State): State => { const destinationIndexNameValid = isValidIndexName(destinationIndexName); const destinationIndexPatternTitleExists = state.indexPatternsMap[destinationIndexName] !== undefined; + + const resultsFieldEmptyString = + typeof jobConfig?.dest?.results_field === 'string' && + jobConfig?.dest?.results_field.trim() === ''; + const mml = jobConfig.model_memory_limit; const modelMemoryLimitEmpty = mml === '' || mml === undefined; if (!modelMemoryLimitEmpty && mml !== undefined) { @@ -292,6 +297,18 @@ export const validateAdvancedEditor = (state: State): State => { }); } + if (resultsFieldEmptyString) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.resultsFieldEmptyString', + { + defaultMessage: 'The results field must not be an empty string.', + } + ), + message: '', + }); + } + if (dependentVariableEmpty) { state.advancedEditorMessages.push({ error: i18n.translate( @@ -336,6 +353,7 @@ export const validateAdvancedEditor = (state: State): State => { sourceIndexNameValid && !destinationIndexNameEmpty && destinationIndexNameValid && + !resultsFieldEmptyString && !dependentVariableEmpty && !modelMemoryLimitEmpty && numTopFeatureImportanceValuesValid && 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 cedbe9094cb20..0d425c8ead4a2 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 @@ -82,6 +82,7 @@ export interface State { previousJobType: null | AnalyticsJobType; requiredFieldsError: string | undefined; randomizeSeed: undefined | number; + resultsField: undefined | string; sourceIndex: EsIndexName; sourceIndexNameEmpty: boolean; sourceIndexNameValid: boolean; @@ -147,6 +148,7 @@ export const getInitialState = (): State => ({ previousJobType: null, requiredFieldsError: undefined, randomizeSeed: undefined, + resultsField: undefined, sourceIndex: '', sourceIndexNameEmpty: true, sourceIndexNameValid: false, @@ -198,6 +200,13 @@ export const getJobConfigFromFormState = ( model_memory_limit: formState.modelMemoryLimit, }; + const resultsFieldEmpty = + typeof formState?.resultsField === 'string' && formState?.resultsField.trim() === ''; + + if (jobConfig.dest && !resultsFieldEmpty) { + jobConfig.dest.results_field = formState.resultsField; + } + if ( formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION @@ -277,6 +286,7 @@ export function getCloneFormStateFromJobConfig( const resultState: Partial = { jobType, description: analyticsJobConfig.description ?? '', + resultsField: analyticsJobConfig.dest.results_field, sourceIndex: Array.isArray(analyticsJobConfig.source.index) ? analyticsJobConfig.source.index.join(',') : analyticsJobConfig.source.index, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index 7d1f456d2334f..a08821c65bfe7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -10,13 +10,12 @@ import { getToastNotifications } from '../../../util/dependency_cache'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { SavedSearchQuery } from '../../../contexts/ml'; +import { OMIT_FIELDS } from '../../../../../common/constants/field_types'; import { IndexPatternTitle } from '../../../../../common/types/kibana'; import { ml } from '../../../services/ml_api_service'; import { FieldRequestConfig } from '../common'; -// List of system fields we don't want to display. -const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; // Maximum number of examples to obtain for text type fields. const MAX_EXAMPLES_DEFAULT: number = 10; diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index 068f43a140c90..f356d79c0a8e1 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -9,12 +9,10 @@ import { useUrlState } from '../../util/url_state'; import { SWIMLANE_TYPE } from '../explorer_constants'; import { AppStateSelectedCells } from '../explorer_utils'; -export const useSelectedCells = (): [ - AppStateSelectedCells | undefined, - (swimlaneSelectedCells: AppStateSelectedCells) => void -] => { - const [appState, setAppState] = useUrlState('_a'); - +export const useSelectedCells = ( + appState: any, + setAppState: ReturnType[1] +): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => { // keep swimlane selection, restore selectedCells from AppState const selectedCells = useMemo(() => { return appState?.mlExplorerSwimlane?.selectedType !== undefined diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 15c54fc5b3a46..569eca4aba949 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -11,6 +11,7 @@ import rison from 'rison-node'; import { mlJobService } from '../../../services/job_service'; import { ml } from '../../../services/ml_api_service'; import { getToastNotifications } from '../../../util/dependency_cache'; +import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { i18n } from '@kbn/i18n'; @@ -350,14 +351,6 @@ export function checkForAutoStartDatafeed() { } } -function stringMatch(str, substr) { - return ( - typeof str === 'string' && - typeof substr === 'string' && - (str.toLowerCase().match(substr.toLowerCase()) === null) === false - ); -} - function jobProperty(job, prop) { const propMap = { job_state: 'jobState', diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index d8c4dab150fb5..29e8aafffef7e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -226,16 +226,23 @@ export class JobCreator { this._calendars = calendars; } - public set modelPlot(enable: boolean) { - if (enable) { - this._job_config.model_plot_config = { - enabled: true, - }; - } else { - delete this._job_config.model_plot_config; + private _initModelPlotConfig() { + // initialize configs to false if they are missing + if (this._job_config.model_plot_config === undefined) { + this._job_config.model_plot_config = {}; + } + if (this._job_config.model_plot_config.enabled === undefined) { + this._job_config.model_plot_config.enabled = false; + } + if (this._job_config.model_plot_config.annotations_enabled === undefined) { + this._job_config.model_plot_config.annotations_enabled = false; } } + public set modelPlot(enable: boolean) { + this._initModelPlotConfig(); + this._job_config.model_plot_config!.enabled = enable; + } public get modelPlot() { return ( this._job_config.model_plot_config !== undefined && @@ -243,6 +250,15 @@ export class JobCreator { ); } + public set modelChangeAnnotations(enable: boolean) { + this._initModelPlotConfig(); + this._job_config.model_plot_config!.annotations_enabled = enable; + } + + public get modelChangeAnnotations() { + return this._job_config.model_plot_config?.annotations_enabled === true; + } + public set useDedicatedIndex(enable: boolean) { this._useDedicatedIndex = enable; if (enable) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx index 9a158f78c39be..18bd6f7fc6e23 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx @@ -14,6 +14,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { ModelPlotSwitch } from './components/model_plot'; +import { AnnotationsSwitch } from './components/annotations'; import { DedicatedIndexSwitch } from './components/dedicated_index'; import { ModelMemoryLimitInput } from '../../../common/model_memory_limit'; import { JobCreatorContext } from '../../../job_creator_context'; @@ -41,6 +42,7 @@ export const AdvancedSection: FC = ({ advancedExpanded, setAdvancedExpand + @@ -68,6 +70,7 @@ export const AdvancedSection: FC = ({ advancedExpanded, setAdvancedExpand > + diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/annotations_switch.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/annotations_switch.tsx new file mode 100644 index 0000000000000..9defbb12207e2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/annotations_switch.tsx @@ -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. + */ + +import React, { FC, useState, useContext, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { JobCreatorContext } from '../../../../../job_creator_context'; +import { Description } from './description'; + +export const AnnotationsSwitch: FC = () => { + const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const [annotationsEnabled, setAnnotationsEnabled] = useState(jobCreator.modelChangeAnnotations); + const [showCallOut, setShowCallout] = useState( + jobCreator.modelPlot && !jobCreator.modelChangeAnnotations + ); + + useEffect(() => { + jobCreator.modelChangeAnnotations = annotationsEnabled; + jobCreatorUpdate(); + }, [annotationsEnabled]); + + useEffect(() => { + setShowCallout(jobCreator.modelPlot && !annotationsEnabled); + }, [jobCreatorUpdated, annotationsEnabled]); + + function toggleAnnotations() { + setAnnotationsEnabled(!annotationsEnabled); + } + + return ( + <> + + + + {showCallOut && ( + + } + color="primary" + iconType="help" + /> + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx new file mode 100644 index 0000000000000..92b07ff8d0910 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +export const Description: FC = memo(({ children }) => { + const title = i18n.translate( + 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.enableModelPlotAnnotations.title', + { + defaultMessage: 'Enable model change annotations', + } + ); + return ( + {title}} + description={ + + } + > + + <>{children} + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/index.ts new file mode 100644 index 0000000000000..04bd97e140055 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AnnotationsSwitch } from './annotations_switch'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index 171c7bbdd550c..48b044e5371de 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -125,6 +125,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { if (jobCreator.type === JOB_TYPE.SINGLE_METRIC) { jobCreator.modelPlot = true; + jobCreator.modelChangeAnnotations = true; } if (mlContext.currentSavedSearch !== null) { diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 281493c4e31b7..f1b8083f19ccf 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -12,6 +12,7 @@ import { IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { MlContext, MlContextValue } from '../contexts/ml'; +import { UrlStateProvider } from '../util/url_state'; import * as routes from './routes'; @@ -48,21 +49,23 @@ export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { return ( -

- {Object.entries(routes).map(([name, route]) => ( - { - window.setTimeout(() => { - setBreadcrumbs(route.breadcrumbs); - }); - return route.render(props, pageDeps); - }} - /> - ))} -
+ +
+ {Object.entries(routes).map(([name, route]) => ( + { + window.setTimeout(() => { + setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, pageDeps); + }} + /> + ))} +
+
); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 52b4408d1ac5b..7a7865c9bd738 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -152,7 +152,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); - const [selectedCells, setSelectedCells] = useSelectedCells(); + const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState); useEffect(() => { explorerService.setSelectedCells(selectedCells); }, [JSON.stringify(selectedCells)]); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.d.ts b/x-pack/plugins/ml/public/application/util/string_utils.d.ts deleted file mode 100644 index 531e44e3e78c1..0000000000000 --- a/x-pack/plugins/ml/public/application/util/string_utils.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function escapeForElasticsearchQuery(str: string): string; - -export function replaceStringTokens( - str: string, - valuesByTokenName: {}, - encodeForURI: boolean -): string; - -export function detectorToString(dtr: any): string; - -export function sortByKey(list: any, reverse: boolean, comparator?: any): any; - -export function toLocaleString(x: number): string; - -export function mlEscape(str: string): string; diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts index 25f1cbd3abac3..034c406afb4b2 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls'; +import { Detector } from '../../../common/types/anomaly_detection_jobs'; + import { replaceStringTokens, detectorToString, - sortByKey, toLocaleString, mlEscape, escapeForElasticsearchQuery, @@ -15,7 +17,7 @@ import { describe('ML - string utils', () => { describe('replaceStringTokens', () => { - const testRecord = { + const testRecord: CustomUrlAnomalyRecordDoc = { job_id: 'test_job', result_type: 'record', probability: 0.0191711, @@ -30,6 +32,10 @@ describe('ML - string utils', () => { testfield1: 'test$tring=[+-?]', testfield2: '{<()>}', testfield3: 'host=\\\\test@uk.dev', + earliest: '0', + latest: '0', + is_interim: false, + initial_record_score: 0, }; test('returns correct values without URI encoding', () => { @@ -68,17 +74,17 @@ describe('ML - string utils', () => { describe('detectorToString', () => { test('returns the correct descriptions for detectors', () => { - const detector1 = { + const detector1: Detector = { function: 'count', }; - const detector2 = { + const detector2: Detector = { function: 'count', by_field_name: 'airline', use_null: false, }; - const detector3 = { + const detector3: Detector = { function: 'mean', field_name: 'CPUUtilization', partition_field_name: 'region', @@ -95,50 +101,6 @@ describe('ML - string utils', () => { }); }); - describe('sortByKey', () => { - const obj = { - zebra: 'stripes', - giraffe: 'neck', - elephant: 'trunk', - }; - - const valueComparator = function (value: string) { - return value; - }; - - test('returns correct ordering with default comparator', () => { - const result = sortByKey(obj, false); - const keys = Object.keys(result); - expect(keys[0]).toBe('elephant'); - expect(keys[1]).toBe('giraffe'); - expect(keys[2]).toBe('zebra'); - }); - - test('returns correct ordering with default comparator and order reversed', () => { - const result = sortByKey(obj, true); - const keys = Object.keys(result); - expect(keys[0]).toBe('zebra'); - expect(keys[1]).toBe('giraffe'); - expect(keys[2]).toBe('elephant'); - }); - - test('returns correct ordering with comparator', () => { - const result = sortByKey(obj, false, valueComparator); - const keys = Object.keys(result); - expect(keys[0]).toBe('giraffe'); - expect(keys[1]).toBe('zebra'); - expect(keys[2]).toBe('elephant'); - }); - - test('returns correct ordering with comparator and order reversed', () => { - const result = sortByKey(obj, true, valueComparator); - const keys = Object.keys(result); - expect(keys[0]).toBe('elephant'); - expect(keys[1]).toBe('zebra'); - expect(keys[2]).toBe('giraffe'); - }); - }); - describe('toLocaleString', () => { test('returns correct comma placement for large numbers', () => { expect(toLocaleString(1)).toBe('1'); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.js b/x-pack/plugins/ml/public/application/util/string_utils.ts similarity index 75% rename from x-pack/plugins/ml/public/application/util/string_utils.js rename to x-pack/plugins/ml/public/application/util/string_utils.ts index 7411820ba3239..aa283fd71bf79 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.js +++ b/x-pack/plugins/ml/public/application/util/string_utils.ts @@ -10,6 +10,9 @@ import _ from 'lodash'; import d3 from 'd3'; +import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls'; +import { Detector } from '../../../common/types/anomaly_detection_jobs'; + // Replaces all instances of dollar delimited tokens in the specified String // with corresponding values from the supplied object, optionally // encoding the replacement for a URI component. @@ -17,7 +20,11 @@ import d3 from 'd3'; // and valuesByTokenName of {"airline":"AAL"}, will return // 'http://www.google.co.uk/#q=airline+code+AAL'. // If a corresponding key is not found in valuesByTokenName, then the String is not replaced. -export function replaceStringTokens(str, valuesByTokenName, encodeForURI) { +export function replaceStringTokens( + str: string, + valuesByTokenName: CustomUrlAnomalyRecordDoc, + encodeForURI: boolean +) { return String(str).replace(/\$([^?&$\'"]+)\$/g, (match, name) => { // Use lodash get to allow nested JSON fields to be retrieved. let tokenValue = _.get(valuesByTokenName, name, null); @@ -31,7 +38,7 @@ export function replaceStringTokens(str, valuesByTokenName, encodeForURI) { } // creates the default description for a given detector -export function detectorToString(dtr) { +export function detectorToString(dtr: Detector): string { const BY_TOKEN = ' by '; const OVER_TOKEN = ' over '; const USE_NULL_OPTION = ' use_null='; @@ -73,7 +80,7 @@ export function detectorToString(dtr) { } // wrap a the inputed string in quotes if it contains non-word characters -function quoteField(field) { +function quoteField(field: string): string { if (field.match(/\W/g)) { return '"' + field + '"'; } else { @@ -81,28 +88,10 @@ function quoteField(field) { } } -// re-order an object based on the value of the keys -export function sortByKey(list, reverse, comparator) { - let keys = _.sortBy(_.keys(list), (key) => { - return comparator ? comparator(list[key], key) : key; - }); - - if (reverse) { - keys = keys.reverse(); - } - - return _.zipObject( - keys, - _.map(keys, (key) => { - return list[key]; - }) - ); -} - // add commas to large numbers // Number.toLocaleString is not supported on safari -export function toLocaleString(x) { - let result = x; +export function toLocaleString(x: number): string { + let result = x.toString(); if (x && typeof x === 'number') { const parts = x.toString().split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); @@ -112,8 +101,8 @@ export function toLocaleString(x) { } // escape html characters -export function mlEscape(str) { - const entityMap = { +export function mlEscape(str: string): string { + const entityMap: { [escapeChar: string]: string } = { '&': '&', '<': '<', '>': '>', @@ -125,7 +114,7 @@ export function mlEscape(str) { } // Escapes reserved characters for use in Elasticsearch query terms. -export function escapeForElasticsearchQuery(str) { +export function escapeForElasticsearchQuery(str: string): string { // Escape with a leading backslash any of the characters that // Elastic document may cause a syntax error when used in queries: // + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ / @@ -133,27 +122,24 @@ export function escapeForElasticsearchQuery(str) { return String(str).replace(/[-[\]{}()+!<>=?:\/\\^"~*&|\s]/g, '\\$&'); } -export function calculateTextWidth(txt, isNumber, elementSelection) { - txt = isNumber ? d3.format(',')(txt) : txt; - let svg = elementSelection; - let $el; - if (elementSelection === undefined) { - // Create a temporary selection to append the label to. - // Note styling of font will be inherited from CSS of page. - const $body = d3.select('body'); - $el = $body.append('div'); - svg = $el.append('svg'); - } +export function calculateTextWidth(txt: string | number, isNumber: boolean) { + txt = isNumber && typeof txt === 'number' ? d3.format(',')(txt) : txt; + + // Create a temporary selection to append the label to. + // Note styling of font will be inherited from CSS of page. + const $body = d3.select('body'); + const $el = $body.append('div'); + const svg = $el.append('svg'); const tempLabelText = svg .append('g') .attr('class', 'temp-axis-label tick') .selectAll('text.temp.axis') - .data('a') + .data(['a']) .enter() .append('text') .text(txt); - const width = tempLabelText[0][0].getBBox().width; + const width = (tempLabelText[0][0] as SVGSVGElement).getBBox().width; d3.select('.temp-axis-label').remove(); if ($el !== undefined) { @@ -161,3 +147,11 @@ export function calculateTextWidth(txt, isNumber, elementSelection) { } return Math.ceil(width); } + +export function stringMatch(str: string | undefined, substr: any) { + return ( + typeof str === 'string' && + typeof substr === 'string' && + (str.toLowerCase().match(substr.toLowerCase()) === null) === false + ); +} diff --git a/x-pack/plugins/ml/public/application/util/url_state.test.ts b/x-pack/plugins/ml/public/application/util/url_state.test.tsx similarity index 82% rename from x-pack/plugins/ml/public/application/util/url_state.test.ts rename to x-pack/plugins/ml/public/application/util/url_state.test.tsx index 0813f2e3da97f..9c03369648554 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.test.ts +++ b/x-pack/plugins/ml/public/application/util/url_state.test.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook, act } from '@testing-library/react-hooks'; -import { getUrlState, useUrlState } from './url_state'; +import React, { FC } from 'react'; +import { render, act } from '@testing-library/react'; +import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; const mockHistoryPush = jest.fn(); @@ -22,7 +23,7 @@ jest.mock('react-router-dom', () => ({ describe('getUrlState', () => { test('properly decode url with _g and _a', () => { expect( - getUrlState( + parseUrlState( "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d" ) ).toEqual({ @@ -64,13 +65,19 @@ describe('useUrlState', () => { }); test('pushes a properly encoded search string to history', () => { - const { result } = renderHook(() => useUrlState('_a')); + const TestComponent: FC = () => { + const [, setUrlState] = useUrlState('_a'); + return ; + }; + + const { getByText } = render( + + + + ); act(() => { - const [, setUrlState] = result.current; - setUrlState({ - query: {}, - }); + getByText('ButtonText').click(); }); expect(mockHistoryPush).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/ml/public/application/util/url_state.ts b/x-pack/plugins/ml/public/application/util/url_state.tsx similarity index 54% rename from x-pack/plugins/ml/public/application/util/url_state.ts rename to x-pack/plugins/ml/public/application/util/url_state.tsx index beff5340ce7e4..c288a00bb06da 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.ts +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -5,7 +5,7 @@ */ import { parse, stringify } from 'query-string'; -import { useCallback } from 'react'; +import React, { createContext, useCallback, useContext, useMemo, FC } from 'react'; import { isEqual } from 'lodash'; import { decode, encode } from 'rison-node'; import { useHistory, useLocation } from 'react-router-dom'; @@ -14,8 +14,16 @@ import { Dictionary } from '../../../common/types/common'; import { getNestedProperty } from './object_utils'; -export type SetUrlState = (attribute: string | Dictionary, value?: any) => void; -export type UrlState = [Dictionary, SetUrlState]; +type Accessor = '_a' | '_g'; +export type SetUrlState = ( + accessor: Accessor, + attribute: string | Dictionary, + value?: any +) => void; +export interface UrlState { + searchString: string; + setUrlState: SetUrlState; +} /** * Set of URL query parameters that require the rison serialization. @@ -30,7 +38,7 @@ function isRisonSerializationRequired(queryParam: string): boolean { return risonSerializedParams.has(queryParam); } -export function getUrlState(search: string): Dictionary { +export function parseUrlState(search: string): Dictionary { const urlState: Dictionary = {}; const parsedQueryString = parse(search, { sort: false }); @@ -56,14 +64,23 @@ export function getUrlState(search: string): Dictionary { // - `history.push()` is the successor of `save`. // - The exposed state and set call make use of the above and make sure that // different urlStates(e.g. `_a` / `_g`) don't overwrite each other. -export const useUrlState = (accessor: string): UrlState => { +// This uses a context to be able to maintain only one instance +// of the url state. It gets passed down with `UrlStateProvider` +// and can be used via `useUrlState`. +export const urlStateStore = createContext({ + searchString: '', + setUrlState: () => {}, +}); +const { Provider } = urlStateStore; +export const UrlStateProvider: FC = ({ children }) => { const history = useHistory(); - const { search } = useLocation(); + const { search: searchString } = useLocation(); - const setUrlState = useCallback( - (attribute: string | Dictionary, value?: any) => { - const urlState = getUrlState(search); - const parsedQueryString = parse(search, { sort: false }); + const setUrlState: SetUrlState = useCallback( + (accessor: Accessor, attribute: string | Dictionary, value?: any) => { + const prevSearchString = searchString; + const urlState = parseUrlState(prevSearchString); + const parsedQueryString = parse(prevSearchString, { sort: false }); if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) { urlState[accessor] = {}; @@ -71,7 +88,7 @@ export const useUrlState = (accessor: string): UrlState => { if (typeof attribute === 'string') { if (isEqual(getNestedProperty(urlState, `${accessor}.${attribute}`), value)) { - return; + return prevSearchString; } urlState[accessor][attribute] = value; @@ -83,7 +100,10 @@ export const useUrlState = (accessor: string): UrlState => { } try { - const oldLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); + const oldLocationSearchString = stringify(parsedQueryString, { + sort: false, + encode: false, + }); Object.keys(urlState).forEach((a) => { if (isRisonSerializationRequired(a)) { @@ -92,20 +112,41 @@ export const useUrlState = (accessor: string): UrlState => { parsedQueryString[a] = urlState[a]; } }); - const newLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); + const newLocationSearchString = stringify(parsedQueryString, { + sort: false, + encode: false, + }); - if (oldLocationSearch !== newLocationSearch) { - history.push({ - search: stringify(parsedQueryString, { sort: false }), - }); + if (oldLocationSearchString !== newLocationSearchString) { + const newSearchString = stringify(parsedQueryString, { sort: false }); + history.push({ search: newSearchString }); } } catch (error) { // eslint-disable-next-line no-console console.error('Could not save url state', error); } }, - [search] + [searchString] ); - return [getUrlState(search)[accessor], setUrlState]; + return {children}; +}; + +export const useUrlState = (accessor: Accessor) => { + const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore); + + const urlState = useMemo(() => { + const fullUrlState = parseUrlState(searchString); + if (typeof fullUrlState === 'object') { + return fullUrlState[accessor]; + } + return undefined; + }, [searchString]); + + const setUrlState = useCallback( + (attribute: string | Dictionary, value?: any) => + setUrlStateContext(accessor, attribute, value), + [accessor, setUrlStateContext] + ); + return [urlState, setUrlState]; }; diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 0b544d4eca0ed..78e05c9a6d07b 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -175,9 +175,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobId } = request.params; + const body = request.body; + const results = await context.ml!.mlClient.callAsCurrentUser('ml.addJob', { jobId, - body: request.body, + body, }); return response.ok({ body: results, diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 21a9fabf445f1..5bc8d96656ed4 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -3,23 +3,64 @@ * 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 { createHashHistory } from 'history'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; -import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; +import { Route, Router, Switch } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; -import { Home } from '../pages/home'; +import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; import { PluginContext } from '../context/plugin_context'; +import { useUrlParams } from '../hooks/use_url_params'; +import { routes } from '../routes'; +import { usePluginContext } from '../hooks/use_plugin_context'; + +const App = () => { + return ( + <> + + {Object.keys(routes).map((key) => { + const path = key as keyof typeof routes; + const route = routes[path]; + const Wrapper = () => { + const { core } = usePluginContext(); + useEffect(() => { + core.chrome.setBreadcrumbs([ + { + text: i18n.translate('xpack.observability.observability.breadcrumb.', { + defaultMessage: 'Observability', + }), + }, + ...route.breadcrumb, + ]); + }, [core]); + + const { query, path: pathParams } = useUrlParams(route.params); + return route.handler({ query, path: pathParams }); + }; + return ; + })} + + + ); +}; export const renderApp = (core: CoreStart, { element }: AppMountParameters) => { const i18nCore = core.i18n; const isDarkMode = core.uiSettings.get('theme:darkMode'); + const history = createHashHistory(); ReactDOM.render( - - - - - + + + + + + + + + , element ); diff --git a/x-pack/plugins/observability/public/assets/illustration_dark.svg b/x-pack/plugins/observability/public/assets/illustration_dark.svg new file mode 100644 index 0000000000000..44815a7455144 --- /dev/null +++ b/x-pack/plugins/observability/public/assets/illustration_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/assets/illustration_light.svg b/x-pack/plugins/observability/public/assets/illustration_light.svg new file mode 100644 index 0000000000000..1690c68fd595a --- /dev/null +++ b/x-pack/plugins/observability/public/assets/illustration_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/assets/observability_overview.png b/x-pack/plugins/observability/public/assets/observability_overview.png deleted file mode 100644 index 70be08af9745a..0000000000000 Binary files a/x-pack/plugins/observability/public/assets/observability_overview.png and /dev/null differ diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx new file mode 100644 index 0000000000000..d09d535a49340 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { ChartContainer } from './'; + +describe('chart container', () => { + it('shows loading indicator', () => { + const component = render( + +
My amazing component
+
+ ); + expect(component.getByTestId('loading')).toBeInTheDocument(); + expect(component.queryByText('My amazing component')).not.toBeInTheDocument(); + }); + it("doesn't show loading indicator", () => { + const component = render( + +
My amazing component
+
+ ); + expect(component.queryByTestId('loading')).not.toBeInTheDocument(); + expect(component.getByText('My amazing component')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx new file mode 100644 index 0000000000000..2a0c25773eae5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx @@ -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 { Chart } from '@elastic/charts'; +import { EuiLoadingChart } from '@elastic/eui'; +import { EuiLoadingChartSize } from '@elastic/eui/src/components/loading/loading_chart'; +import React from 'react'; + +interface Props { + isInitialLoad: boolean; + height?: number; + width?: number; + iconSize?: EuiLoadingChartSize; + children: React.ReactNode; +} + +const CHART_HEIGHT = 170; + +export const ChartContainer = ({ + isInitialLoad, + children, + iconSize = 'xl', + height = CHART_HEIGHT, +}: Props) => { + if (isInitialLoad) { + return ( +
+ +
+ ); + } + return {children}; +}; diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx new file mode 100644 index 0000000000000..e04e8f050006a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ISection } from '../../../typings/section'; +import { render } from '../../../utils/test_helper'; +import { EmptySection } from './'; + +describe('EmptySection', () => { + it('renders without action button', () => { + const section: ISection = { + id: 'apm', + title: 'APM', + icon: 'logoAPM', + description: 'foo bar', + }; + const { getByText, queryAllByText } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('foo bar')).toBeInTheDocument(); + expect(queryAllByText('Install agent')).toEqual([]); + }); + it('renders with action button', () => { + const section: ISection = { + id: 'apm', + title: 'APM', + icon: 'logoAPM', + description: 'foo bar', + linkTitle: 'install agent', + href: 'https://www.elastic.co', + }; + const { getByText, getByTestId } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('foo bar')).toBeInTheDocument(); + const linkButton = getByTestId('empty-apm') as HTMLAnchorElement; + expect(linkButton.href).toEqual('https://www.elastic.co/'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx new file mode 100644 index 0000000000000..e19bf1678bc01 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiButton, EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import React from 'react'; +import { ISection } from '../../../typings/section'; + +interface Props { + section: ISection; +} + +export const EmptySection = ({ section }: Props) => { + return ( + {section.title}} + titleSize="xs" + body={{section.description}} + actions={ + <> + {section.linkTitle && ( + + {section.linkTitle} + + )} + + } + /> + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/header/index.test.tsx b/x-pack/plugins/observability/public/components/app/header/index.test.tsx new file mode 100644 index 0000000000000..59b6fbe9caf7a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/header/index.test.tsx @@ -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 React from 'react'; +import { render } from '../../../utils/test_helper'; +import { Header } from './'; + +describe('Header', () => { + it('renders without add data button', () => { + const { getByText, queryAllByText, getByTestId } = render(
); + expect(getByTestId('observability-logo')).toBeInTheDocument(); + expect(getByText('Observability')).toBeInTheDocument(); + expect(queryAllByText('Add data')).toEqual([]); + }); + it('renders with add data button', () => { + const { getByText, getByTestId } = render(
); + expect(getByTestId('observability-logo')).toBeInTheDocument(); + expect(getByText('Observability')).toBeInTheDocument(); + expect(getByText('Add data')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx new file mode 100644 index 0000000000000..1c6ce766d0901 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBetaBadge, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +const Container = styled.div<{ color: string }>` + background: ${(props) => props.color}; + border-bottom: ${(props) => props.theme.eui.euiBorderThin}; +`; + +const Wrapper = styled.div<{ restrictWidth?: number }>` + width: 100%; + max-width: ${(props) => `${props.restrictWidth}px`}; + margin: 0 auto; + overflow: hidden; + padding: ${(props) => (props.restrictWidth ? 0 : '0 24px')}; +`; + +interface Props { + color: string; + showAddData?: boolean; + restrictWidth?: number; + showGiveFeedback?: boolean; +} + +export const Header = ({ + color, + restrictWidth, + showAddData = false, + showGiveFeedback = false, +}: Props) => { + const { core } = usePluginContext(); + return ( + + + + + + + + + +

+ {i18n.translate('xpack.observability.home.title', { + defaultMessage: 'Observability', + })}{' '} + +

+
+
+ {showGiveFeedback && ( + + + {i18n.translate('xpack.observability.home.feedback', { + defaultMessage: 'Give us feedback', + })} + + + )} + {showAddData && ( + + + {i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })} + + + )} +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx new file mode 100644 index 0000000000000..27b25f0056055 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { Header } from '../header/index'; + +const getPaddingSize = (props: EuiPageProps) => (props.restrictWidth ? 0 : '24px'); + +const Page = styled(EuiPage)` + background: transparent; + padding-right: ${getPaddingSize}; + padding-left: ${getPaddingSize}; +`; + +const Container = styled.div<{ color?: string }>` + overflow-y: hidden; + min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); + background: ${(props) => props.color}; +`; + +interface Props { + headerColor: string; + bodyColor: string; + children?: React.ReactNode; + restrictWidth?: number; + showAddData?: boolean; + showGiveFeedback?: boolean; +} + +export const WithHeaderLayout = ({ + headerColor, + bodyColor, + children, + restrictWidth, + showAddData, + showGiveFeedback, +}: Props) => ( + +
+ + {children} + + +); diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.scss b/x-pack/plugins/observability/public/components/app/news_feed/index.scss new file mode 100644 index 0000000000000..1222fe489c732 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.scss @@ -0,0 +1,3 @@ +.obsNewsFeed__itemImg{ + @include euiBottomShadowSmall; +} \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx new file mode 100644 index 0000000000000..c71130b57c33f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.test.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 from 'react'; +import { NewsItem } from '../../../services/get_news_feed'; +import { render } from '../../../utils/test_helper'; +import { NewsFeed } from './'; + +const newsFeedItems = [ + { + title: { + en: 'Elastic introduces OpenTelemetry integration', + }, + description: { + en: + 'We are pleased to announce the availability of the Elastic OpenTelemetry integration — available on Elastic Cloud, or when you download Elastic APM.', + }, + link_url: { + en: + 'https://www.elastic.co/blog/elastic-apm-opentelemetry-integration?blade=observabilitysolutionfeed', + }, + image_url: { + en: 'foo.png', + }, + }, + { + title: { + en: 'Kubernetes observability tutorial: Log monitoring and analysis', + }, + description: { + en: + 'Learn how Elastic Observability makes it easy to monitor and detect anomalies in millions of logs from thousands of containers running hundreds of microservices — while Kubernetes scales applications with changing pod counts. All from a single UI.', + }, + link_url: { + en: + 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-log-monitoring-and-analysis-elastic-stack?blade=observabilitysolutionfeed', + }, + image_url: null, + }, + { + title: { + en: 'Kubernetes observability tutorial: K8s cluster setup and demo app deployment', + }, + description: { + en: + 'This blog will walk you through configuring the environment you will be using for the Kubernetes observability tutorial blog series. We will be deploying Elasticsearch Service, a Minikube single-node Kubernetes cluster setup, and a demo app.', + }, + link_url: { + en: + 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-cluster-setup-demo-app-deployment?blade=observabilitysolutionfeed', + }, + image_url: { + en: null, + }, + }, +] as NewsItem[]; +describe('News', () => { + it('renders resources with all elements', () => { + const { getByText, getAllByText, queryAllByTestId } = render( + + ); + expect(getByText("What's new")).toBeInTheDocument(); + expect(getAllByText('Read full story').length).toEqual(3); + expect(queryAllByTestId('news_image').length).toEqual(1); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx new file mode 100644 index 0000000000000..2fbd6659bcb5a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + EuiErrorBoundary, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { truncate } from 'lodash'; +import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { NewsItem as INewsItem } from '../../../services/get_news_feed'; +import './index.scss'; + +interface Props { + items: INewsItem[]; +} + +export const NewsFeed = ({ items }: Props) => { + return ( + // The news feed is manually added/edited, to prevent any errors caused by typos or missing fields, + // wraps the component with EuiErrorBoundary to avoid breaking the entire page. + + + + +

+ {i18n.translate('xpack.observability.news.title', { + defaultMessage: "What's new", + })} +

+
+
+ {items.map((item, index) => ( + + + + ))} +
+
+ ); +}; + +const limitString = (string: string, limit: number) => truncate(string, { length: limit }); + +const NewsItem = ({ item }: { item: INewsItem }) => { + const theme = useContext(ThemeContext); + + return ( + + + +

{item.title.en}

+
+
+ + + + + + + {limitString(item.description.en, 128)} + + + + + + {i18n.translate('xpack.observability.news.readFullStory', { + defaultMessage: 'Read full story', + })} + + + + + + {item.image_url?.en && ( + + {item.title.en} + + )} + + + +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/resources/index.test.tsx b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx new file mode 100644 index 0000000000000..570aa3954424f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { Resources } from './'; + +describe('Resources', () => { + it('renders resources with all elements', () => { + const { getByText } = render(); + expect(getByText('Documentation')).toBeInTheDocument(); + expect(getByText('Discuss forum')).toBeInTheDocument(); + expect(getByText('Observability fundamentals')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx new file mode 100644 index 0000000000000..c330c358d022a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/resources/index.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 { EuiFlexGroup, EuiFlexItem, EuiListGroup, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +const resources = [ + { + iconType: 'documents', + label: i18n.translate('xpack.observability.resources.documentation', { + defaultMessage: 'Documentation', + }), + href: 'https://www.elastic.co/guide/en/observability/current/observability-ui.html', + }, + { + iconType: 'editorComment', + label: i18n.translate('xpack.observability.resources.forum', { + defaultMessage: 'Discuss forum', + }), + href: 'https://discuss.elastic.co/c/observability/', + }, + { + iconType: 'training', + label: i18n.translate('xpack.observability.resources.training', { + defaultMessage: 'Observability fundamentals', + }), + href: 'https://www.elastic.co/training/observability-fundamentals', + }, +]; + +export const Resources = () => { + return ( + + + +

+ {i18n.translate('xpack.observability.resources.title', { + defaultMessage: 'Resources', + })} +

+
+
+ +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx new file mode 100644 index 0000000000000..4c80195d33ace --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx @@ -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 { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIconTip, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useState } from 'react'; +import { EuiSelect } from '@elastic/eui'; +import { uniqBy } from 'lodash'; +import { Alert } from '../../../../../../alerts/common'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { SectionContainer } from '..'; + +const ALL_TYPES = 'ALL_TYPES'; +const allTypes = { + value: ALL_TYPES, + text: i18n.translate('xpack.observability.overview.alert.allTypes', { + defaultMessage: 'All types', + }), +}; + +interface Props { + alerts: Alert[]; +} + +export const AlertsSection = ({ alerts }: Props) => { + const { core } = usePluginContext(); + const [filter, setFilter] = useState(ALL_TYPES); + + const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({ + value: consumer, + text: consumer, + })); + + return ( + + + + + + setFilter(e.target.value)} + prepend={i18n.translate('xpack.observability.overview.alert.view', { + defaultMessage: 'View', + })} + /> + + + + + + {alerts + .filter((alert) => filter === ALL_TYPES || alert.consumer === filter) + .map((alert, index) => { + const isLastElement = index === alerts.length - 1; + return ( + + + + {alert.name} + + + + + + {alert.alertTypeId} + + {alert.tags.map((tag, idx) => { + return ( + + {tag} + + ); + })} + + + + + + + Updated {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago + + + {alert.muteAll && ( + + + + )} + + + {!isLastElement && } + + ); + })} + + + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx new file mode 100644 index 0000000000000..d4b8236e0ef49 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 * as fetcherHook from '../../../../hooks/use_fetcher'; +import { render } from '../../../../utils/test_helper'; +import { APMSection } from './'; +import { response } from './mock_data/apm.mock'; + +describe('APMSection', () => { + it('renders with transaction series and stats', () => { + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: response, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const { getByText, queryAllByTestId } = render( + + ); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('View in app')).toBeInTheDocument(); + expect(getByText('Services 11')).toBeInTheDocument(); + expect(getByText('Transactions per minute 312.00k')).toBeInTheDocument(); + expect(queryAllByTestId('loading')).toEqual([]); + }); + it('shows loading state', () => { + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: undefined, + status: fetcherHook.FETCH_STATUS.LOADING, + refetch: jest.fn(), + }); + const { getByText, queryAllByText, getByTestId } = render( + + ); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByTestId('loading')).toBeInTheDocument(); + expect(queryAllByText('View in app')).toEqual([]); + expect(queryAllByText('Services 11')).toEqual([]); + expect(queryAllByText('Transactions per minute 312.00k')).toEqual([]); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx new file mode 100644 index 0000000000000..697d4adfa0b75 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ThemeContext } from 'styled-components'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; +import { onBrushEnd } from '../helper'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +function formatTpm(value?: number) { + return numeral(value).format('0.00a'); +} + +export const APMSection = ({ startTime, endTime, bucketSize }: Props) => { + const theme = useContext(ThemeContext); + const history = useHistory(); + + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('apm')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const { title = 'APM', appLink, stats, series } = data || {}; + + const min = moment.utc(startTime).valueOf(); + const max = moment.utc(endTime).valueOf(); + + const formatter = niceTimeFormatter([min, max]); + + const isLoading = status === FETCH_STATUS.LOADING; + + const transactionsColor = theme.eui.euiColorVis1; + + return ( + + + + + + + + + + + onBrushEnd({ x, history })} + theme={useChartTheme()} + showLegend={false} + xDomain={{ min, max }} + /> + {series?.transactions.coordinates && ( + <> + + `${formatTpm(value)} tpm`} + /> + + + )} + + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts b/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts new file mode 100644 index 0000000000000..5857021b1537f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ApmFetchDataResponse } from '../../../../../typings'; + +export const response: ApmFetchDataResponse = { + title: 'APM', + + appLink: '/app/apm', + stats: { + services: { value: 11, type: 'number' }, + transactions: { value: 312000, type: 'number' }, + }, + series: { + transactions: { + coordinates: [ + { x: 1591365600000, y: 32 }, + { x: 1591366200000, y: 43 }, + { x: 1591366800000, y: 22 }, + { x: 1591367400000, y: 29 }, + { x: 1591368000000, y: 39 }, + { x: 1591368600000, y: 36 }, + { x: 1591369200000, y: 50 }, + { x: 1591369800000, y: 31 }, + { x: 1591370400000, y: 39 }, + { x: 1591371000000, y: 26 }, + { x: 1591371600000, y: 45 }, + { x: 1591372200000, y: 27 }, + { x: 1591372800000, y: 37 }, + { x: 1591373400000, y: 55 }, + { x: 1591374000000, y: 31 }, + { x: 1591374600000, y: 26 }, + { x: 1591375200000, y: 57 }, + { x: 1591375800000, y: 25 }, + { x: 1591376400000, y: 28 }, + { x: 1591377000000, y: 40 }, + { x: 1591377600000, y: 33 }, + { x: 1591378200000, y: 33 }, + { x: 1591378800000, y: 31 }, + { x: 1591379400000, y: 32 }, + { x: 1591380000000, y: 34 }, + { x: 1591380600000, y: 31 }, + { x: 1591381200000, y: 16 }, + { x: 1591381800000, y: 34 }, + { x: 1591382400000, y: 33 }, + { x: 1591383000000, y: 35 }, + { x: 1591383600000, y: 47 }, + { x: 1591384200000, y: 44 }, + { x: 1591384800000, y: 21 }, + { x: 1591385400000, y: 25 }, + { x: 1591386000000, y: 34 }, + { x: 1591386600000, y: 37 }, + { x: 1591387200000, y: 38 }, + { x: 1591387800000, y: 28 }, + { x: 1591388400000, y: 32 }, + { x: 1591389000000, y: 37 }, + { x: 1591389600000, y: 25 }, + { x: 1591390200000, y: 33 }, + { x: 1591390800000, y: 34 }, + { x: 1591391400000, y: 30 }, + { x: 1591392000000, y: 45 }, + { x: 1591392600000, y: 42 }, + { x: 1591393200000, y: 23 }, + { x: 1591393800000, y: 33 }, + { x: 1591394400000, y: 38 }, + { x: 1591395000000, y: 30 }, + { x: 1591395600000, y: 25 }, + { x: 1591396200000, y: 33 }, + { x: 1591396800000, y: 37 }, + { x: 1591397400000, y: 43 }, + { x: 1591398000000, y: 30 }, + { x: 1591398600000, y: 36 }, + { x: 1591399200000, y: 28 }, + { x: 1591399800000, y: 39 }, + { x: 1591400400000, y: 27 }, + { x: 1591401000000, y: 41 }, + { x: 1591401600000, y: 25 }, + { x: 1591402200000, y: 31 }, + { x: 1591402800000, y: 28 }, + { x: 1591403400000, y: 29 }, + { x: 1591404000000, y: 49 }, + { x: 1591404600000, y: 24 }, + { x: 1591405200000, y: 41 }, + { x: 1591405800000, y: 30 }, + { x: 1591406400000, y: 36 }, + { x: 1591407000000, y: 39 }, + { x: 1591407600000, y: 23 }, + { x: 1591408200000, y: 40 }, + { x: 1591408800000, y: 34 }, + { x: 1591409400000, y: 28 }, + { x: 1591410000000, y: 33 }, + { x: 1591410600000, y: 31 }, + { x: 1591411200000, y: 39 }, + { x: 1591411800000, y: 33 }, + { x: 1591412400000, y: 35 }, + { x: 1591413000000, y: 31 }, + { x: 1591413600000, y: 35 }, + { x: 1591414200000, y: 37 }, + { x: 1591414800000, y: 26 }, + { x: 1591415400000, y: 27 }, + { x: 1591416000000, y: 26 }, + { x: 1591416600000, y: 34 }, + { x: 1591417200000, y: 33 }, + { x: 1591417800000, y: 38 }, + { x: 1591418400000, y: 34 }, + { x: 1591419000000, y: 37 }, + { x: 1591419600000, y: 24 }, + { x: 1591420200000, y: 25 }, + { x: 1591420800000, y: 20 }, + { x: 1591421400000, y: 35 }, + { x: 1591422000000, y: 41 }, + { x: 1591422600000, y: 40 }, + { x: 1591423200000, y: 33 }, + { x: 1591423800000, y: 24 }, + { x: 1591424400000, y: 44 }, + { x: 1591425000000, y: 24 }, + { x: 1591425600000, y: 32 }, + { x: 1591426200000, y: 37 }, + { x: 1591426800000, y: 34 }, + { x: 1591427400000, y: 28 }, + { x: 1591428000000, y: 26 }, + { x: 1591428600000, y: 37 }, + { x: 1591429200000, y: 36 }, + { x: 1591429800000, y: 37 }, + { x: 1591430400000, y: 23 }, + { x: 1591431000000, y: 47 }, + { x: 1591431600000, y: 41 }, + { x: 1591432200000, y: 24 }, + { x: 1591432800000, y: 34 }, + { x: 1591433400000, y: 27 }, + { x: 1591434000000, y: 34 }, + { x: 1591434600000, y: 44 }, + { x: 1591435200000, y: 20 }, + { x: 1591435800000, y: 34 }, + { x: 1591436400000, y: 29 }, + { x: 1591437000000, y: 28 }, + { x: 1591437600000, y: 36 }, + { x: 1591438200000, y: 34 }, + { x: 1591438800000, y: 26 }, + { x: 1591439400000, y: 29 }, + { x: 1591440000000, y: 45 }, + { x: 1591440600000, y: 34 }, + { x: 1591441200000, y: 25 }, + { x: 1591441800000, y: 34 }, + { x: 1591442400000, y: 28 }, + { x: 1591443000000, y: 34 }, + { x: 1591443600000, y: 31 }, + { x: 1591444200000, y: 24 }, + { x: 1591444800000, y: 34 }, + { x: 1591445400000, y: 21 }, + { x: 1591446000000, y: 40 }, + { x: 1591446600000, y: 37 }, + { x: 1591447200000, y: 31 }, + { x: 1591447800000, y: 21 }, + { x: 1591448400000, y: 24 }, + { x: 1591449000000, y: 30 }, + { x: 1591449600000, y: 22 }, + { x: 1591450200000, y: 27 }, + { x: 1591450800000, y: 30 }, + { x: 1591451400000, y: 22 }, + { x: 1591452000000, y: 9 }, + ], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx new file mode 100644 index 0000000000000..8f0781b8f0269 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx @@ -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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const ErrorPanel = () => { + return ( + + + + {i18n.translate('xpack.observability.section.errorPanel', { + defaultMessage: 'An error happened when trying to fetch data. Please try again', + })} + + + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/helper.test.ts b/x-pack/plugins/observability/public/components/app/section/helper.test.ts new file mode 100644 index 0000000000000..6a8cd27753a8d --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/helper.test.ts @@ -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 { onBrushEnd } from './helper'; +import { History } from 'history'; + +describe('Chart helper', () => { + describe('onBrushEnd', () => { + const history = ({ + push: jest.fn(), + location: { + search: '', + }, + } as unknown) as History; + it("doesn't push a new history when x is not defined", () => { + onBrushEnd({ x: undefined, history }); + expect(history.push).not.toBeCalled(); + }); + + it('pushes a new history with time range converted to ISO', () => { + onBrushEnd({ x: [1593409448167, 1593415727797], history }); + expect(history.push).toBeCalledWith({ + search: 'rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z', + }); + }); + + it('pushes a new history keeping current search', () => { + history.location.search = '?foo=bar'; + onBrushEnd({ x: [1593409448167, 1593415727797], history }); + expect(history.push).toBeCalledWith({ + search: 'foo=bar&rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z', + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/helper.ts b/x-pack/plugins/observability/public/components/app/section/helper.ts new file mode 100644 index 0000000000000..81fa92cb87782 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/helper.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { XYBrushArea } from '@elastic/charts'; +import { History } from 'history'; +import { fromQuery, toQuery } from '../../../utils/url'; + +export const onBrushEnd = ({ x, history }: { x: XYBrushArea['x']; history: History }) => { + if (x) { + const start = x[0]; + const end = x[1]; + + const currentSearch = toQuery(history.location.search); + const nextSearch = { + rangeFrom: new Date(start).toISOString(), + rangeTo: new Date(end).toISOString(), + }; + history.push({ + ...history.location, + search: fromQuery({ + ...currentSearch, + ...nextSearch, + }), + }); + } +}; diff --git a/x-pack/plugins/observability/public/components/app/section/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/index.test.tsx new file mode 100644 index 0000000000000..49cb175d0c094 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/index.test.tsx @@ -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 React from 'react'; +import { render } from '../../../utils/test_helper'; +import { SectionContainer } from './'; + +describe('SectionContainer', () => { + it('renders section without app link', () => { + const component = render( + +
I am a very nice component
+
+ ); + expect(component.getByText('I am a very nice component')).toBeInTheDocument(); + expect(component.getByText('Foo')).toBeInTheDocument(); + expect(component.queryAllByText('View in app')).toEqual([]); + }); + it('renders section with app link', () => { + const component = render( + +
I am a very nice component
+
+ ); + expect(component.getByText('I am a very nice component')).toBeInTheDocument(); + expect(component.getByText('Foo')).toBeInTheDocument(); + expect(component.getByText('View in app')).toBeInTheDocument(); + }); + it('renders section with error', () => { + const component = render( + +
I am a very nice component
+
+ ); + expect(component.queryByText('I am a very nice component')).not.toBeInTheDocument(); + expect(component.getByText('Foo')).toBeInTheDocument(); + expect( + component.getByText('An error happened when trying to fetch data. Please try again') + ).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx new file mode 100644 index 0000000000000..3556e8c01ab30 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/index.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ErrorPanel } from './error_panel'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +interface Props { + title: string; + hasError: boolean; + children: React.ReactNode; + minHeight?: number; + appLink?: string; + appLinkName?: string; +} + +export const SectionContainer = ({ title, appLink, children, hasError, appLinkName }: Props) => { + const { core } = usePluginContext(); + return ( + +
{title}
+ + } + extraAction={ + appLink && ( + + + {appLinkName + ? appLinkName + : i18n.translate('xpack.observability.chart.viewInAppLabel', { + defaultMessage: 'View in app', + })} + + + ) + } + > + <> + + + {hasError ? ( + + ) : ( + <> + + {children} + + )} + + +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx new file mode 100644 index 0000000000000..f3ba2ef6fa83a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { isEmpty } from 'lodash'; +import moment from 'moment'; +import React, { Fragment } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { LogsFetchDataResponse } from '../../../../typings'; +import { formatStatValue } from '../../../../utils/format_stat_value'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; +import { onBrushEnd } from '../helper'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +function getColorPerItem(series?: LogsFetchDataResponse['series']) { + if (!series) { + return {}; + } + const availableColors = euiPaletteColorBlind({ + rotations: Math.ceil(Object.keys(series).length / 10), + }); + const colorsPerItem = Object.keys(series).reduce((acc: Record, key, index) => { + acc[key] = availableColors[index]; + return acc; + }, {}); + + return colorsPerItem; +} + +export const LogsSection = ({ startTime, endTime, bucketSize }: Props) => { + const history = useHistory(); + + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('infra_logs')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const min = moment.utc(startTime).valueOf(); + const max = moment.utc(endTime).valueOf(); + + const formatter = niceTimeFormatter([min, max]); + + const { title, appLink, stats, series } = data || {}; + + const colorsPerItem = getColorPerItem(series); + + const isLoading = status === FETCH_STATUS.LOADING; + + return ( + + +

+ {i18n.translate('xpack.observability.overview.logs.subtitle', { + defaultMessage: 'Logs rate per minute', + })} +

+
+ + + {!stats || isEmpty(stats) ? ( + + + + ) : ( + Object.keys(stats).map((key) => { + const stat = stats[key]; + return ( + + + + ); + }) + )} + + + onBrushEnd({ x, history })} + theme={useChartTheme()} + showLegend + legendPosition={Position.Right} + xDomain={{ min, max }} + showLegendExtra + /> + {series && + Object.keys(series).map((key) => { + const serie = series[key]; + const chartData = serie.coordinates.map((coordinate) => ({ + ...coordinate, + g: serie.label, + })); + return ( + + + + numeral(d).format('0a')} + /> + + ); + })} + +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx new file mode 100644 index 0000000000000..6276e1ba1baca --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AreaSeries, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { Series } from '../../../../typings'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +/** + * EuiProgress doesn't support custom color, when it does this component can be removed. + */ +const StyledProgress = styled.div<{ color?: string }>` + progress { + &.euiProgress--native { + &::-webkit-progress-value { + background-color: ${(props) => props.color}; + } + + &::-moz-progress-bar { + background-color: ${(props) => props.color}; + } + } + + &.euiProgress--indeterminate { + &:before { + background-color: ${(props) => props.color}; + } + } + } +`; + +export const MetricsSection = ({ startTime, endTime, bucketSize }: Props) => { + const theme = useContext(ThemeContext); + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('infra_metrics')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const isLoading = status === FETCH_STATUS.LOADING; + + const { title = 'Metrics', appLink, stats, series } = data || {}; + + const cpuColor = theme.eui.euiColorVis7; + const memoryColor = theme.eui.euiColorVis0; + const inboundTrafficColor = theme.eui.euiColorVis3; + const outboundTrafficColor = theme.eui.euiColorVis2; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const AreaChart = ({ + serie, + isLoading, + color, +}: { + serie?: Series; + isLoading: boolean; + color: string; +}) => { + const chartTheme = useChartTheme(); + + return ( + + + {serie && ( + + )} + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx new file mode 100644 index 0000000000000..1f8ca6e61f132 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Axis, + BarSeries, + niceTimeFormatter, + Position, + ScaleType, + Settings, + TickFormatter, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ThemeContext } from 'styled-components'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { Series } from '../../../../typings'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; +import { onBrushEnd } from '../helper'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +export const UptimeSection = ({ startTime, endTime, bucketSize }: Props) => { + const theme = useContext(ThemeContext); + const history = useHistory(); + + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('uptime')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const min = moment.utc(startTime).valueOf(); + const max = moment.utc(endTime).valueOf(); + const formatter = niceTimeFormatter([min, max]); + + const isLoading = status === FETCH_STATUS.LOADING; + + const { title = 'Uptime', appLink, stats, series } = data || {}; + + const downColor = theme.eui.euiColorVis2; + const upColor = theme.eui.euiColorLightShade; + + return ( + + + {/* Stats section */} + + + + + + + + + + + + {/* Chart section */} + + onBrushEnd({ x, history })} + theme={useChartTheme()} + showLegend={false} + legendPosition={Position.Right} + xDomain={{ min, max }} + /> + + + + + ); +}; + +const UptimeBarSeries = ({ + id, + label, + series, + color, + ticktFormatter, +}: { + id: string; + label: string; + series?: Series; + color: string; + ticktFormatter: TickFormatter; +}) => { + if (!series) { + return null; + } + const chartData = series.coordinates.map((coordinate) => ({ + ...coordinate, + g: label, + })); + return ( + <> + + + numeral(x).format('0a')} + /> + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx new file mode 100644 index 0000000000000..fe38df6484c29 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import styled from 'styled-components'; +import { EuiStat } from '@elastic/eui'; +import React from 'react'; +import { EuiStatProps } from '@elastic/eui/src/components/stat/stat'; + +const Stat = styled(EuiStat)` + .euiStat__title { + color: ${(props) => props.color}; + } +`; + +interface Props extends Partial { + children?: React.ReactNode; + color?: string; +} + +const EMPTY_VALUE = '--'; + +export const StyledStat = (props: Props) => { + const { description = EMPTY_VALUE, title = EMPTY_VALUE, ...rest } = props; + return ; +}; diff --git a/x-pack/plugins/observability/public/components/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx similarity index 100% rename from x-pack/plugins/observability/public/components/action_menu.tsx rename to x-pack/plugins/observability/public/components/shared/action_menu/index.tsx diff --git a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx new file mode 100644 index 0000000000000..cc77c1ed72b4a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/data_picker/index.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 { EuiSuperDatePicker } from '@elastic/eui'; +import React from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings'; +import { fromQuery, toQuery } from '../../../utils/url'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +export interface TimePickerRefreshInterval { + pause: boolean; + value: number; +} + +interface Props { + rangeFrom: string; + rangeTo: string; + refreshPaused: boolean; + refreshInterval: number; +} + +export const DatePicker = ({ rangeFrom, rangeTo, refreshPaused, refreshInterval }: Props) => { + const location = useLocation(); + const history = useHistory(); + + const timePickerQuickRanges = useKibanaUISettings( + UI_SETTINGS.TIMEPICKER_QUICK_RANGES + ); + + const commonlyUsedRanges = timePickerQuickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + + function updateUrl(nextQuery: { + rangeFrom?: string; + rangeTo?: string; + refreshPaused?: boolean; + refreshInterval?: number; + }) { + history.push({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + ...nextQuery, + }), + }); + } + + function onRefreshChange({ + isPaused, + refreshInterval: interval, + }: { + isPaused: boolean; + refreshInterval: number; + }) { + updateUrl({ refreshPaused: isPaused, refreshInterval: interval }); + } + + function onTimeChange({ start, end }: { start: string; end: string }) { + updateUrl({ rangeFrom: start, rangeTo: end }); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 39e702a332a8e..d7f8c471ad9aa 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -17,9 +17,20 @@ export function registerDataHandler({ dataHandlers[appName] = { fetchData, hasData }; } +export function unregisterDataHandler({ appName }: { appName: T }) { + delete dataHandlers[appName]; +} + export function getDataHandler(appName: T) { const dataHandler = dataHandlers[appName]; if (dataHandler) { return dataHandler as DataHandler; } } + +export async function fetchHasData() { + const apps: ObservabilityApp[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics']; + const promises = apps.map((app) => getDataHandler(app)?.hasData()); + const [apm, uptime, logs, metrics] = await Promise.all(promises); + return { apm, uptime, infra_logs: logs, infra_metrics: metrics }; +} diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx new file mode 100644 index 0000000000000..13f7159ba6043 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; + +export function useChartTheme() { + const theme = useContext(ThemeContext); + return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetcher.tsx b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx new file mode 100644 index 0000000000000..88a8ad264e737 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, useMemo } from 'react'; + +export enum FETCH_STATUS { + LOADING = 'loading', + SUCCESS = 'success', + FAILURE = 'failure', + PENDING = 'pending', +} + +export interface FetcherResult { + data?: Data; + status: FETCH_STATUS; + error?: Error; +} + +// fetcher functions can return undefined OR a promise. Previously we had a more simple type +// but it led to issues when using object destructuring with default values +type InferResponseType = Exclude extends Promise + ? TResponseType + : unknown; + +export function useFetcher( + fn: () => TReturn, + fnDeps: any[], + options: { + preservePreviousData?: boolean; + } = {} +): FetcherResult> & { refetch: () => void } { + const { preservePreviousData = true } = options; + + const [result, setResult] = useState>>({ + data: undefined, + status: FETCH_STATUS.PENDING, + }); + const [counter, setCounter] = useState(0); + useEffect(() => { + async function doFetch() { + const promise = fn(); + if (!promise) { + return; + } + + setResult((prevResult) => ({ + data: preservePreviousData ? prevResult.data : undefined, + status: FETCH_STATUS.LOADING, + error: undefined, + })); + + try { + const data = await promise; + setResult({ + data, + status: FETCH_STATUS.SUCCESS, + error: undefined, + } as FetcherResult>); + } catch (e) { + setResult((prevResult) => ({ + data: preservePreviousData ? prevResult.data : undefined, + status: FETCH_STATUS.FAILURE, + error: e, + })); + } + } + + doFetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [counter, ...fnDeps]); + + return useMemo(() => { + return { + ...result, + refetch: () => { + // this will invalidate the deps to `useEffect` and will result in a new request + setCounter((count) => count + 1); + }, + }; + }, [result]); +} diff --git a/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx b/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx new file mode 100644 index 0000000000000..884d74db391ee --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { usePluginContext } from './use_plugin_context'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; + +export { UI_SETTINGS }; + +type SettingKeys = keyof typeof UI_SETTINGS; +type SettingValues = typeof UI_SETTINGS[SettingKeys]; + +export function useKibanaUISettings(key: SettingValues): T { + const { core } = usePluginContext(); + return core.uiSettings.get(key); +} diff --git a/x-pack/plugins/observability/public/hooks/use_url_params.tsx b/x-pack/plugins/observability/public/hooks/use_url_params.tsx new file mode 100644 index 0000000000000..680a32fb49677 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_url_params.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 * as t from 'io-ts'; +import { useLocation, useParams } from 'react-router-dom'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { Params } from '../routes'; + +function getQueryParams(location: ReturnType) { + const urlSearchParms = new URLSearchParams(location.search); + const queryParams: Record = {}; + urlSearchParms.forEach((value, key) => { + queryParams[key] = value; + }); + return queryParams; +} + +/** + * Extracts query and path params from the url and validate it against the type defined in the route file. + * It removes any aditional item which is not declared in the type. + * @param params + */ +export function useUrlParams(params: Params) { + const location = useLocation(); + const pathParams = useParams(); + const queryParams = getQueryParams(location); + + const rts = { + queryRt: params.query ? t.exact(params.query) : t.strict({}), + pathRt: params.path ? t.exact(params.path) : t.strict({}), + }; + + const queryResult = rts.queryRt.decode(queryParams); + const pathResult = rts.pathRt.decode(pathParams); + if (isLeft(queryResult)) { + // eslint-disable-next-line no-console + console.error(PathReporter.report(queryResult)[0]); + } + + if (isLeft(pathResult)) { + // eslint-disable-next-line no-console + console.error(PathReporter.report(pathResult)[0]); + } + + return { + query: isLeft(queryResult) ? {} : queryResult.right, + path: isLeft(pathResult) ? {} : pathResult.right, + }; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index d2f1d246f79ec..03939736b64ae 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -15,7 +15,7 @@ export const plugin: PluginInitializer props.theme.eui.euiColorEmptyShade}; -`; - -const Title = styled.div` - background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; - border-bottom: ${(props) => props.theme.eui.euiBorderThin}; -`; - -const Page = styled.div` - width: 100%; - max-width: 1200px; - margin: 0 auto; - overflow: hidden; -} -`; - -const EuiCardWithoutPadding = styled(EuiCard)` - padding: 0; -`; - -export const Home = () => { - const { core } = usePluginContext(); - - useEffect(() => { - core.chrome.setBreadcrumbs([ - { - text: i18n.translate('xpack.observability.home.breadcrumb.observability', { - defaultMessage: 'Observability', - }), - }, - { - text: i18n.translate('xpack.observability.home.breadcrumb.gettingStarted', { - defaultMessage: 'Getting started', - }), - }, - ]); - }, [core]); - - return ( - - - <Page> - <EuiSpacer size="xxl" /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiIcon type="logoObservability" size="xxl" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size="m"> - <h1> - {i18n.translate('xpack.observability.home.title', { - defaultMessage: 'Observability', - })} - </h1> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="xxl" /> - </Page> - - - - - {/* title and description */} - - -

- {i18n.translate('xpack.observability.home.sectionTitle', { - defaultMessage: 'Unified visibility across your entire ecosystem', - })} -

-
- - - {i18n.translate('xpack.observability.home.sectionsubtitle', { - defaultMessage: - 'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.', - })} - -
- - {/* Apps sections */} - - - - - - {appsSection.map((app) => ( - - } - title={ - -

{app.title}

-
- } - description={app.description} - /> -
- ))} -
-
- - - -
-
- - {/* Get started button */} - - - - - {i18n.translate('xpack.observability.home.getStatedButton', { - defaultMessage: 'Get started', - })} - - - - -
-
-
- ); +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { fetchHasData } from '../../data_handler'; +import { useFetcher } from '../../hooks/use_fetcher'; + +export const HomePage = () => { + const history = useHistory(); + const { data = {} } = useFetcher(() => fetchHasData(), []); + + const values = Object.values(data); + const hasSomeData = values.length ? values.some((hasData) => hasData) : null; + + if (hasSomeData === true) { + history.push({ pathname: '/overview' }); + } + if (hasSomeData === false) { + history.push({ pathname: '/landing' }); + } + + return <>; }; diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts index d33571a16ccb7..8c87f17c16b3d 100644 --- a/x-pack/plugins/observability/public/pages/home/section.ts +++ b/x-pack/plugins/observability/public/pages/home/section.ts @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; - -interface ISection { - id: string; - title: string; - icon: string; - description: string; - href?: string; - target?: '_blank'; -} +import { ISection } from '../../typings/section'; export const appsSection: ISection[] = [ { - id: 'logs', + id: 'infra_logs', title: i18n.translate('xpack.observability.section.apps.logs.title', { defaultMessage: 'Logs', }), @@ -25,6 +17,7 @@ export const appsSection: ISection[] = [ defaultMessage: 'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.', }), + href: 'https://www.elastic.co', }, { id: 'apm', @@ -36,9 +29,10 @@ export const appsSection: ISection[] = [ defaultMessage: 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', }), + href: 'https://www.elastic.co', }, { - id: 'metrics', + id: 'infra_metrics', title: i18n.translate('xpack.observability.section.apps.metrics.title', { defaultMessage: 'Metrics', }), @@ -47,6 +41,7 @@ export const appsSection: ISection[] = [ defaultMessage: 'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.', }), + href: 'https://www.elastic.co', }, { id: 'uptime', @@ -58,5 +53,6 @@ export const appsSection: ISection[] = [ defaultMessage: 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.', }), + href: 'https://www.elastic.co', }, ]; diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx new file mode 100644 index 0000000000000..512f4428d9bf2 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiCard, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiImage, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { appsSection } from '../home/section'; + +const EuiCardWithoutPadding = styled(EuiCard)` + padding: 0; +`; + +export const LandingPage = () => { + const { core } = usePluginContext(); + const theme = useContext(ThemeContext); + + return ( + + + {/* title and description */} + + +

+ {i18n.translate('xpack.observability.home.sectionTitle', { + defaultMessage: 'Unified visibility across your entire ecosystem', + })} +

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

{app.title}

+
+ } + description={app.description} + /> +
+ ))} +
+
+ + + +
+
+ + + + {/* Get started button */} + + + + + {i18n.translate('xpack.observability.home.getStatedButton', { + defaultMessage: 'Get started', + })} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts new file mode 100644 index 0000000000000..61456bc88bd3e --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { AppMountContext } from 'kibana/public'; +import { ISection } from '../../typings/section'; + +export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): ISection[] => { + return [ + { + id: 'infra_logs', + title: i18n.translate('xpack.observability.emptySection.apps.logs.title', { + defaultMessage: 'Logs', + }), + icon: 'logoLogging', + description: i18n.translate('xpack.observability.emptySection.apps.logs.description', { + defaultMessage: + 'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.logs.link', { + defaultMessage: 'Install Filebeat', + }), + href: 'https://www.elastic.co', + }, + { + id: 'apm', + title: i18n.translate('xpack.observability.emptySection.apps.apm.title', { + defaultMessage: 'APM', + }), + icon: 'logoAPM', + description: i18n.translate('xpack.observability.emptySection.apps.apm.description', { + defaultMessage: + 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.apm.link', { + defaultMessage: 'Install agent', + }), + href: 'https://www.elastic.co', + }, + { + id: 'infra_metrics', + title: i18n.translate('xpack.observability.emptySection.apps.metrics.title', { + defaultMessage: 'Metrics', + }), + icon: 'logoMetrics', + description: i18n.translate('xpack.observability.emptySection.apps.metrics.description', { + defaultMessage: + 'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.metrics.link', { + defaultMessage: 'Install metrics module', + }), + href: 'https://www.elastic.co', + }, + { + id: 'uptime', + title: i18n.translate('xpack.observability.emptySection.apps.uptime.title', { + defaultMessage: 'Uptime', + }), + icon: 'logoUptime', + description: i18n.translate('xpack.observability.emptySection.apps.uptime.description', { + defaultMessage: + 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', { + defaultMessage: 'Install Heartbeat', + }), + href: 'https://www.elastic.co', + }, + { + id: 'alert', + title: i18n.translate('xpack.observability.emptySection.apps.alert.title', { + defaultMessage: 'No alerts found.', + }), + icon: 'watchesApp', + description: i18n.translate('xpack.observability.emptySection.apps.alert.description', { + defaultMessage: + '503 errors stacking up. Applications not responding. CPU and RAM utilization jumping. See these warnings as they happen - not as part of the post-mortem.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', { + defaultMessage: 'Create alert', + }), + href: core.http.basePath.prepend( + '/app/management/insightsAndAlerting/triggersActions/alerts' + ), + }, + ]; +}; diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx new file mode 100644 index 0000000000000..3674e69ab5702 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import moment from 'moment'; +import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { EmptySection } from '../../components/app/empty_section'; +import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { Resources } from '../../components/app/resources'; +import { AlertsSection } from '../../components/app/section/alerts'; +import { APMSection } from '../../components/app/section/apm'; +import { LogsSection } from '../../components/app/section/logs'; +import { MetricsSection } from '../../components/app/section/metrics'; +import { UptimeSection } from '../../components/app/section/uptime'; +import { DatePicker, TimePickerTime } from '../../components/shared/data_picker'; +import { NewsFeed } from '../../components/app/news_feed'; +import { fetchHasData } from '../../data_handler'; +import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; +import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { RouteParams } from '../../routes'; +import { getObservabilityAlerts } from '../../services/get_observability_alerts'; +import { getParsedDate } from '../../utils/date'; +import { getBucketSize } from '../../utils/get_bucket_size'; +import { getEmptySections } from './empty_section'; +import { LoadingObservability } from './loading_observability'; +import { getNewsFeed } from '../../services/get_news_feed'; + +interface Props { + routeParams: RouteParams<'/overview'>; +} + +function calculatetBucketSize({ startTime, endTime }: { startTime?: string; endTime?: string }) { + if (startTime && endTime) { + return getBucketSize({ + start: moment.utc(startTime).valueOf(), + end: moment.utc(endTime).valueOf(), + minInterval: '60s', + }); + } +} + +export const OverviewPage = ({ routeParams }: Props) => { + const { core } = usePluginContext(); + + const { data: alerts = [], status: alertStatus } = useFetcher(() => { + return getObservabilityAlerts({ core }); + }, []); + + const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), []); + + const theme = useContext(ThemeContext); + const timePickerTime = useKibanaUISettings(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); + + const result = useFetcher(() => fetchHasData(), []); + const hasData = result.data; + + if (!hasData) { + return ; + } + + const { + rangeFrom = timePickerTime.from, + rangeTo = timePickerTime.to, + refreshInterval = 10000, + refreshPaused = true, + } = routeParams.query; + + const startTime = getParsedDate(rangeFrom); + const endTime = getParsedDate(rangeTo, { roundUp: true }); + const bucketSize = calculatetBucketSize({ startTime, endTime }); + + const appEmptySections = getEmptySections({ core }).filter(({ id }) => { + if (id === 'alert') { + return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; + } + return !hasData[id]; + }); + + // Hides the data section when all 'hasData' is false or undefined + const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData); + + return ( + + + + + + + + + + + + {/* Data sections */} + {showDataSections && ( + + {hasData.infra_logs && ( + + + + )} + {hasData.infra_metrics && ( + + + + )} + {hasData.apm && ( + + + + )} + {hasData.uptime && ( + + + + )} + + )} + + {/* Empty sections */} + {!!appEmptySections.length && ( + + + 2 ? 2 : 1 + } + gutterSize="s" + > + {appEmptySections.map((app) => { + return ( + + + + ); + })} + + + )} + + + {/* Alert section */} + {!!alerts.length && ( + + + + )} + + {/* Resources section */} + + + + + + + {!!newsFeed?.items?.length && ( + + + + )} + + + + + ); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx new file mode 100644 index 0000000000000..90e3104443e6b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, { useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { WithHeaderLayout } from '../../components/app/layout/with_header'; + +const CentralizedFlexGroup = styled(EuiFlexGroup)` + justify-content: center; + align-items: center; + // place the element in the center of the page + min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); +`; + +export const LoadingObservability = () => { + const theme = useContext(ThemeContext); + + return ( + + + + + + + + + + + {i18n.translate('xpack.observability.overview.loadingObservability', { + defaultMessage: 'Loading Observability', + })} + + + + + + + + ); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts new file mode 100644 index 0000000000000..759b8b5fdae4f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 alertsFetchData = async () => { + return Promise.resolve({ + data: [ + { + id: '1', + consumer: 'apm', + name: 'Error rate | opbeans-java', + alertTypeId: 'apm.error_rate', + tags: ['apm', 'service.name:opbeans-java'], + updatedAt: '2020-07-03T14:27:51.488Z', + muteAll: true, + }, + { + id: '2', + consumer: 'apm', + name: 'Transaction duration | opbeans-java', + alertTypeId: 'apm.transaction_duration', + tags: ['apm', 'service.name:opbeans-java'], + updatedAt: '2020-07-02T14:27:51.488Z', + muteAll: true, + }, + { + id: '3', + consumer: 'logs', + name: 'Logs obs test', + alertTypeId: 'logs.alert.document.count', + tags: ['logs', 'observability'], + updatedAt: '2020-06-30T14:27:51.488Z', + muteAll: true, + }, + { + id: '4', + consumer: 'metrics', + name: 'Metrics obs test', + alertTypeId: 'metrics.alert.inventory.threshold', + tags: ['metrics', 'observability'], + updatedAt: '2020-03-20T14:27:51.488Z', + muteAll: true, + }, + { + id: '5', + consumer: 'uptime', + name: 'Uptime obs test', + alertTypeId: 'xpack.uptime.alerts.monitorStatus', + tags: ['uptime', 'observability'], + updatedAt: '2020-03-25T17:27:51.488Z', + muteAll: true, + }, + ], + }); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts new file mode 100644 index 0000000000000..7303b78cc0132 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts @@ -0,0 +1,627 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ApmFetchDataResponse, FetchData } from '../../../typings'; + +export const fetchApmData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: ApmFetchDataResponse = { + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + value: 7, + }, + transactions: { + type: 'number', + value: 125808, + }, + }, + series: { + transactions: { + coordinates: [ + { + x: 1593295200000, + y: 891, + }, + { + x: 1593297000000, + y: 902, + }, + { + x: 1593298800000, + y: 924, + }, + { + x: 1593300600000, + y: 944, + }, + { + x: 1593302400000, + y: 935, + }, + { + x: 1593304200000, + y: 915, + }, + { + x: 1593306000000, + y: 917, + }, + { + x: 1593307800000, + y: 941, + }, + { + x: 1593309600000, + y: 906, + }, + { + x: 1593311400000, + y: 939, + }, + { + x: 1593313200000, + y: 961, + }, + { + x: 1593315000000, + y: 911, + }, + { + x: 1593316800000, + y: 958, + }, + { + x: 1593318600000, + y: 861, + }, + { + x: 1593320400000, + y: 906, + }, + { + x: 1593322200000, + y: 899, + }, + { + x: 1593324000000, + y: 785, + }, + { + x: 1593325800000, + y: 952, + }, + { + x: 1593327600000, + y: 910, + }, + { + x: 1593329400000, + y: 869, + }, + { + x: 1593331200000, + y: 895, + }, + { + x: 1593333000000, + y: 924, + }, + { + x: 1593334800000, + y: 930, + }, + { + x: 1593336600000, + y: 947, + }, + { + x: 1593338400000, + y: 905, + }, + { + x: 1593340200000, + y: 963, + }, + { + x: 1593342000000, + y: 877, + }, + { + x: 1593343800000, + y: 839, + }, + { + x: 1593345600000, + y: 884, + }, + { + x: 1593347400000, + y: 934, + }, + { + x: 1593349200000, + y: 908, + }, + { + x: 1593351000000, + y: 982, + }, + { + x: 1593352800000, + y: 897, + }, + { + x: 1593354600000, + y: 903, + }, + { + x: 1593356400000, + y: 877, + }, + { + x: 1593358200000, + y: 893, + }, + { + x: 1593360000000, + y: 919, + }, + { + x: 1593361800000, + y: 844, + }, + { + x: 1593363600000, + y: 940, + }, + { + x: 1593365400000, + y: 951, + }, + { + x: 1593367200000, + y: 869, + }, + { + x: 1593369000000, + y: 901, + }, + { + x: 1593370800000, + y: 940, + }, + { + x: 1593372600000, + y: 942, + }, + { + x: 1593374400000, + y: 881, + }, + { + x: 1593376200000, + y: 935, + }, + { + x: 1593378000000, + y: 892, + }, + { + x: 1593379800000, + y: 861, + }, + { + x: 1593381600000, + y: 868, + }, + { + x: 1593383400000, + y: 990, + }, + { + x: 1593385200000, + y: 931, + }, + { + x: 1593387000000, + y: 898, + }, + { + x: 1593388800000, + y: 906, + }, + { + x: 1593390600000, + y: 928, + }, + { + x: 1593392400000, + y: 975, + }, + { + x: 1593394200000, + y: 842, + }, + { + x: 1593396000000, + y: 940, + }, + { + x: 1593397800000, + y: 922, + }, + { + x: 1593399600000, + y: 962, + }, + { + x: 1593401400000, + y: 940, + }, + { + x: 1593403200000, + y: 974, + }, + { + x: 1593405000000, + y: 887, + }, + { + x: 1593406800000, + y: 920, + }, + { + x: 1593408600000, + y: 854, + }, + { + x: 1593410400000, + y: 898, + }, + { + x: 1593412200000, + y: 952, + }, + { + x: 1593414000000, + y: 987, + }, + { + x: 1593415800000, + y: 932, + }, + { + x: 1593417600000, + y: 1009, + }, + { + x: 1593419400000, + y: 989, + }, + { + x: 1593421200000, + y: 939, + }, + { + x: 1593423000000, + y: 929, + }, + { + x: 1593424800000, + y: 929, + }, + { + x: 1593426600000, + y: 864, + }, + { + x: 1593428400000, + y: 895, + }, + { + x: 1593430200000, + y: 876, + }, + { + x: 1593432000000, + y: 68, + }, + { + x: 1593433800000, + y: 0, + }, + { + x: 1593435600000, + y: 0, + }, + { + x: 1593437400000, + y: 0, + }, + { + x: 1593439200000, + y: 0, + }, + { + x: 1593441000000, + y: 0, + }, + { + x: 1593442800000, + y: 700, + }, + { + x: 1593444600000, + y: 930, + }, + { + x: 1593446400000, + y: 953, + }, + { + x: 1593448200000, + y: 995, + }, + { + x: 1593450000000, + y: 883, + }, + { + x: 1593451800000, + y: 902, + }, + { + x: 1593453600000, + y: 988, + }, + { + x: 1593455400000, + y: 947, + }, + { + x: 1593457200000, + y: 889, + }, + { + x: 1593459000000, + y: 982, + }, + { + x: 1593460800000, + y: 919, + }, + { + x: 1593462600000, + y: 854, + }, + { + x: 1593464400000, + y: 894, + }, + { + x: 1593466200000, + y: 901, + }, + { + x: 1593468000000, + y: 970, + }, + { + x: 1593469800000, + y: 840, + }, + { + x: 1593471600000, + y: 857, + }, + { + x: 1593473400000, + y: 943, + }, + { + x: 1593475200000, + y: 825, + }, + { + x: 1593477000000, + y: 955, + }, + { + x: 1593478800000, + y: 959, + }, + { + x: 1593480600000, + y: 921, + }, + { + x: 1593482400000, + y: 924, + }, + { + x: 1593484200000, + y: 840, + }, + { + x: 1593486000000, + y: 943, + }, + { + x: 1593487800000, + y: 919, + }, + { + x: 1593489600000, + y: 882, + }, + { + x: 1593491400000, + y: 900, + }, + { + x: 1593493200000, + y: 930, + }, + { + x: 1593495000000, + y: 854, + }, + { + x: 1593496800000, + y: 905, + }, + { + x: 1593498600000, + y: 922, + }, + { + x: 1593500400000, + y: 863, + }, + { + x: 1593502200000, + y: 966, + }, + { + x: 1593504000000, + y: 910, + }, + { + x: 1593505800000, + y: 851, + }, + { + x: 1593507600000, + y: 867, + }, + { + x: 1593509400000, + y: 904, + }, + { + x: 1593511200000, + y: 913, + }, + { + x: 1593513000000, + y: 889, + }, + { + x: 1593514800000, + y: 907, + }, + { + x: 1593516600000, + y: 965, + }, + { + x: 1593518400000, + y: 868, + }, + { + x: 1593520200000, + y: 919, + }, + { + x: 1593522000000, + y: 945, + }, + { + x: 1593523800000, + y: 883, + }, + { + x: 1593525600000, + y: 902, + }, + { + x: 1593527400000, + y: 900, + }, + { + x: 1593529200000, + y: 829, + }, + { + x: 1593531000000, + y: 919, + }, + { + x: 1593532800000, + y: 942, + }, + { + x: 1593534600000, + y: 924, + }, + { + x: 1593536400000, + y: 958, + }, + { + x: 1593538200000, + y: 867, + }, + { + x: 1593540000000, + y: 844, + }, + { + x: 1593541800000, + y: 976, + }, + { + x: 1593543600000, + y: 937, + }, + { + x: 1593545400000, + y: 891, + }, + { + x: 1593547200000, + y: 936, + }, + { + x: 1593549000000, + y: 895, + }, + { + x: 1593550800000, + y: 850, + }, + { + x: 1593552600000, + y: 899, + }, + ], + }, + }, +}; + +export const emptyResponse: ApmFetchDataResponse = { + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + value: 0, + }, + transactions: { + type: 'number', + value: 0, + }, + }, + series: { + transactions: { + coordinates: [], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts new file mode 100644 index 0000000000000..5bea1fbf19ace --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts @@ -0,0 +1,2326 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FetchData, LogsFetchDataResponse } from '../../../typings'; + +export const fetchLogsData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: LogsFetchDataResponse = { + title: 'Logs', + appLink: + "/app/logs/stream?logPosition=(end:'2020-06-30T21:30:00.000Z',start:'2020-06-27T22:00:00.000Z')", + stats: { + 'haproxy.log': { + type: 'number', + label: 'haproxy.log', + value: 145.84289044289045, + }, + 'nginx.access': { + type: 'number', + label: 'nginx.access', + value: 94.67039627039627, + }, + 'kibana.log': { + type: 'number', + label: 'kibana.log', + value: 11.018181818181818, + }, + 'nginx.error': { + type: 'number', + label: 'nginx.error', + value: 8.218181818181819, + }, + }, + series: { + 'haproxy.log': { + label: 'haproxy.log', + coordinates: [ + { + x: 1593295200000, + y: 146.83333333333334, + }, + { + x: 1593297000000, + y: 146.96666666666667, + }, + { + x: 1593298800000, + y: 146.96666666666667, + }, + { + x: 1593300600000, + y: 146.86666666666667, + }, + { + x: 1593302400000, + y: 146.96666666666667, + }, + { + x: 1593304200000, + y: 147.03333333333333, + }, + { + x: 1593306000000, + y: 147.16666666666666, + }, + { + x: 1593307800000, + y: 146.96666666666667, + }, + { + x: 1593309600000, + y: 146.96666666666667, + }, + { + x: 1593311400000, + y: 146.96666666666667, + }, + { + x: 1593313200000, + y: 147.03333333333333, + }, + { + x: 1593315000000, + y: 147.13333333333333, + }, + { + x: 1593316800000, + y: 146.96666666666667, + }, + { + x: 1593318600000, + y: 146.96666666666667, + }, + { + x: 1593320400000, + y: 146.93333333333334, + }, + { + x: 1593322200000, + y: 147.06666666666666, + }, + { + x: 1593324000000, + y: 146.9, + }, + { + x: 1593325800000, + y: 147.06666666666666, + }, + { + x: 1593327600000, + y: 147.06666666666666, + }, + { + x: 1593329400000, + y: 146.93333333333334, + }, + { + x: 1593331200000, + y: 146.86666666666667, + }, + { + x: 1593333000000, + y: 146.86666666666667, + }, + { + x: 1593334800000, + y: 147, + }, + { + x: 1593336600000, + y: 146.66666666666666, + }, + { + x: 1593338400000, + y: 146.83333333333334, + }, + { + x: 1593340200000, + y: 146.9, + }, + { + x: 1593342000000, + y: 146.96666666666667, + }, + { + x: 1593343800000, + y: 146.86666666666667, + }, + { + x: 1593345600000, + y: 146.83333333333334, + }, + { + x: 1593347400000, + y: 146.86666666666667, + }, + { + x: 1593349200000, + y: 146.93333333333334, + }, + { + x: 1593351000000, + y: 146.8, + }, + { + x: 1593352800000, + y: 146.83333333333334, + }, + { + x: 1593354600000, + y: 146.83333333333334, + }, + { + x: 1593356400000, + y: 146.73333333333332, + }, + { + x: 1593358200000, + y: 146.9, + }, + { + x: 1593360000000, + y: 146.73333333333332, + }, + { + x: 1593361800000, + y: 146.63333333333333, + }, + { + x: 1593363600000, + y: 146.6, + }, + { + x: 1593365400000, + y: 147.06666666666666, + }, + { + x: 1593367200000, + y: 147, + }, + { + x: 1593369000000, + y: 146.93333333333334, + }, + { + x: 1593370800000, + y: 146.73333333333332, + }, + { + x: 1593372600000, + y: 147.06666666666666, + }, + { + x: 1593374400000, + y: 147, + }, + { + x: 1593376200000, + y: 147.06666666666666, + }, + { + x: 1593378000000, + y: 147.2, + }, + { + x: 1593379800000, + y: 147.1, + }, + { + x: 1593381600000, + y: 147, + }, + { + x: 1593383400000, + y: 147.06666666666666, + }, + { + x: 1593385200000, + y: 147.13333333333333, + }, + { + x: 1593387000000, + y: 147.2, + }, + { + x: 1593388800000, + y: 146.96666666666667, + }, + { + x: 1593390600000, + y: 146.83333333333334, + }, + { + x: 1593392400000, + y: 146.8, + }, + { + x: 1593394200000, + y: 144.3, + }, + { + x: 1593396000000, + y: 147.3, + }, + { + x: 1593397800000, + y: 147.2, + }, + { + x: 1593399600000, + y: 147.33333333333334, + }, + { + x: 1593401400000, + y: 147.1, + }, + { + x: 1593403200000, + y: 147.13333333333333, + }, + { + x: 1593405000000, + y: 147.16666666666666, + }, + { + x: 1593406800000, + y: 147.1, + }, + { + x: 1593408600000, + y: 147.3, + }, + { + x: 1593410400000, + y: 147.26666666666668, + }, + { + x: 1593412200000, + y: 147.2, + }, + { + x: 1593414000000, + y: 147.03333333333333, + }, + { + x: 1593415800000, + y: 146.9, + }, + { + x: 1593417600000, + y: 146.96666666666667, + }, + { + x: 1593419400000, + y: 147.1, + }, + { + x: 1593421200000, + y: 147.13333333333333, + }, + { + x: 1593423000000, + y: 147.03333333333333, + }, + { + x: 1593424800000, + y: 141.36666666666667, + }, + { + x: 1593426600000, + y: 144.63333333333333, + }, + { + x: 1593428400000, + y: 153.66666666666666, + }, + { + x: 1593430200000, + y: 136.76666666666668, + }, + { + x: 1593432000000, + y: 123.43333333333334, + }, + { + x: 1593433800000, + y: 123.5, + }, + { + x: 1593435600000, + y: 123.26666666666667, + }, + { + x: 1593437400000, + y: 123.23333333333333, + }, + { + x: 1593439200000, + y: 123.13333333333334, + }, + { + x: 1593441000000, + y: 123.2, + }, + { + x: 1593442800000, + y: 144.23333333333332, + }, + { + x: 1593444600000, + y: 147.06666666666666, + }, + { + x: 1593446400000, + y: 146.9, + }, + { + x: 1593448200000, + y: 146.7, + }, + { + x: 1593450000000, + y: 146.8, + }, + { + x: 1593451800000, + y: 146.73333333333332, + }, + { + x: 1593453600000, + y: 146.7, + }, + { + x: 1593455400000, + y: 146.7, + }, + { + x: 1593457200000, + y: 146.56666666666666, + }, + { + x: 1593459000000, + y: 146.8, + }, + { + x: 1593460800000, + y: 146.8, + }, + { + x: 1593462600000, + y: 146.83333333333334, + }, + { + x: 1593464400000, + y: 146.7, + }, + { + x: 1593466200000, + y: 146.9, + }, + { + x: 1593468000000, + y: 147.03333333333333, + }, + { + x: 1593469800000, + y: 146.76666666666668, + }, + { + x: 1593471600000, + y: 146.7, + }, + { + x: 1593473400000, + y: 146.63333333333333, + }, + { + x: 1593475200000, + y: 146.93333333333334, + }, + { + x: 1593477000000, + y: 146.5, + }, + { + x: 1593478800000, + y: 146.76666666666668, + }, + { + x: 1593480600000, + y: 144.83333333333334, + }, + { + x: 1593482400000, + y: 146.96666666666667, + }, + { + x: 1593484200000, + y: 147.1, + }, + { + x: 1593486000000, + y: 147.1, + }, + { + x: 1593487800000, + y: 147.3, + }, + { + x: 1593489600000, + y: 147.1, + }, + { + x: 1593491400000, + y: 147.03333333333333, + }, + { + x: 1593493200000, + y: 147.2, + }, + { + x: 1593495000000, + y: 147.06666666666666, + }, + { + x: 1593496800000, + y: 147.1, + }, + { + x: 1593498600000, + y: 147.2, + }, + { + x: 1593500400000, + y: 147.06666666666666, + }, + { + x: 1593502200000, + y: 147.06666666666666, + }, + { + x: 1593504000000, + y: 147.06666666666666, + }, + { + x: 1593505800000, + y: 147.06666666666666, + }, + { + x: 1593507600000, + y: 146.96666666666667, + }, + { + x: 1593509400000, + y: 147.16666666666666, + }, + { + x: 1593511200000, + y: 147.03333333333333, + }, + { + x: 1593513000000, + y: 147, + }, + { + x: 1593514800000, + y: 147.03333333333333, + }, + { + x: 1593516600000, + y: 146.96666666666667, + }, + { + x: 1593518400000, + y: 146.63333333333333, + }, + { + x: 1593520200000, + y: 146.43333333333334, + }, + { + x: 1593522000000, + y: 147.13333333333333, + }, + { + x: 1593523800000, + y: 147.13333333333333, + }, + { + x: 1593525600000, + y: 146.93333333333334, + }, + { + x: 1593527400000, + y: 147, + }, + { + x: 1593529200000, + y: 147.03333333333333, + }, + { + x: 1593531000000, + y: 147.2, + }, + { + x: 1593532800000, + y: 147.13333333333333, + }, + { + x: 1593534600000, + y: 147.13333333333333, + }, + { + x: 1593536400000, + y: 147.13333333333333, + }, + { + x: 1593538200000, + y: 147.1, + }, + { + x: 1593540000000, + y: 147, + }, + { + x: 1593541800000, + y: 147.26666666666668, + }, + { + x: 1593543600000, + y: 146.73333333333332, + }, + { + x: 1593545400000, + y: 147.03333333333333, + }, + { + x: 1593547200000, + y: 147, + }, + { + x: 1593549000000, + y: 146.9, + }, + { + x: 1593550800000, + y: 147.03333333333333, + }, + ], + }, + 'nginx.access': { + label: 'nginx.access', + coordinates: [ + { + x: 1593295200000, + y: 94.06666666666666, + }, + { + x: 1593297000000, + y: 91.4, + }, + { + x: 1593298800000, + y: 95.03333333333333, + }, + { + x: 1593300600000, + y: 94.5, + }, + { + x: 1593302400000, + y: 94.06666666666666, + }, + { + x: 1593304200000, + y: 93.3, + }, + { + x: 1593306000000, + y: 91.16666666666667, + }, + { + x: 1593307800000, + y: 94.5, + }, + { + x: 1593309600000, + y: 93.53333333333333, + }, + { + x: 1593311400000, + y: 118.9, + }, + { + x: 1593313200000, + y: 110.66666666666667, + }, + { + x: 1593315000000, + y: 95.66666666666667, + }, + { + x: 1593316800000, + y: 99.53333333333333, + }, + { + x: 1593318600000, + y: 123.36666666666666, + }, + { + x: 1593320400000, + y: 94.13333333333334, + }, + { + x: 1593322200000, + y: 95.53333333333333, + }, + { + x: 1593324000000, + y: 93.93333333333334, + }, + { + x: 1593325800000, + y: 94.06666666666666, + }, + { + x: 1593327600000, + y: 118.16666666666667, + }, + { + x: 1593329400000, + y: 108.6, + }, + { + x: 1593331200000, + y: 93.53333333333333, + }, + { + x: 1593333000000, + y: 93.06666666666666, + }, + { + x: 1593334800000, + y: 93.76666666666667, + }, + { + x: 1593336600000, + y: 95.3, + }, + { + x: 1593338400000, + y: 96.4, + }, + { + x: 1593340200000, + y: 121.93333333333334, + }, + { + x: 1593342000000, + y: 134.43333333333334, + }, + { + x: 1593343800000, + y: 160.4, + }, + { + x: 1593345600000, + y: 129.7, + }, + { + x: 1593347400000, + y: 119.16666666666667, + }, + { + x: 1593349200000, + y: 133.06666666666666, + }, + { + x: 1593351000000, + y: 212.4, + }, + { + x: 1593352800000, + y: 95.36666666666666, + }, + { + x: 1593354600000, + y: 93.6, + }, + { + x: 1593356400000, + y: 93.4, + }, + { + x: 1593358200000, + y: 95.1, + }, + { + x: 1593360000000, + y: 94.36666666666666, + }, + { + x: 1593361800000, + y: 97.23333333333333, + }, + { + x: 1593363600000, + y: 94.03333333333333, + }, + { + x: 1593365400000, + y: 94.53333333333333, + }, + { + x: 1593367200000, + y: 93.56666666666666, + }, + { + x: 1593369000000, + y: 98.43333333333334, + }, + { + x: 1593370800000, + y: 92.3, + }, + { + x: 1593372600000, + y: 93.13333333333334, + }, + { + x: 1593374400000, + y: 93.16666666666667, + }, + { + x: 1593376200000, + y: 93.7, + }, + { + x: 1593378000000, + y: 94.46666666666667, + }, + { + x: 1593379800000, + y: 97.16666666666667, + }, + { + x: 1593381600000, + y: 94.36666666666666, + }, + { + x: 1593383400000, + y: 93.7, + }, + { + x: 1593385200000, + y: 93.4, + }, + { + x: 1593387000000, + y: 91.3, + }, + { + x: 1593388800000, + y: 92.66666666666667, + }, + { + x: 1593390600000, + y: 93.73333333333333, + }, + { + x: 1593392400000, + y: 94.33333333333333, + }, + { + x: 1593394200000, + y: 93.23333333333333, + }, + { + x: 1593396000000, + y: 93.9, + }, + { + x: 1593397800000, + y: 92.83333333333333, + }, + { + x: 1593399600000, + y: 93, + }, + { + x: 1593401400000, + y: 91.2, + }, + { + x: 1593403200000, + y: 91.96666666666667, + }, + { + x: 1593405000000, + y: 93.83333333333333, + }, + { + x: 1593406800000, + y: 93.16666666666667, + }, + { + x: 1593408600000, + y: 95.36666666666666, + }, + { + x: 1593410400000, + y: 92.5, + }, + { + x: 1593412200000, + y: 93.16666666666667, + }, + { + x: 1593414000000, + y: 92.8, + }, + { + x: 1593415800000, + y: 95.83333333333333, + }, + { + x: 1593417600000, + y: 96.96666666666667, + }, + { + x: 1593419400000, + y: 94.63333333333334, + }, + { + x: 1593421200000, + y: 98.7, + }, + { + x: 1593423000000, + y: 100.03333333333333, + }, + { + x: 1593424800000, + y: 108.66666666666667, + }, + { + x: 1593426600000, + y: 110.9, + }, + { + x: 1593428400000, + y: 88.56666666666666, + }, + { + x: 1593430200000, + y: 1, + }, + { + x: 1593442800000, + y: 74.53333333333333, + }, + { + x: 1593444600000, + y: 99.03333333333333, + }, + { + x: 1593446400000, + y: 98.03333333333333, + }, + { + x: 1593448200000, + y: 91.26666666666667, + }, + { + x: 1593450000000, + y: 107.76666666666667, + }, + { + x: 1593451800000, + y: 98.26666666666667, + }, + { + x: 1593453600000, + y: 99.46666666666667, + }, + { + x: 1593455400000, + y: 102.33333333333333, + }, + { + x: 1593457200000, + y: 108.13333333333334, + }, + { + x: 1593459000000, + y: 95.36666666666666, + }, + { + x: 1593460800000, + y: 98.23333333333333, + }, + { + x: 1593462600000, + y: 91.46666666666667, + }, + { + x: 1593464400000, + y: 115.63333333333334, + }, + { + x: 1593466200000, + y: 116.23333333333333, + }, + { + x: 1593468000000, + y: 91.66666666666667, + }, + { + x: 1593469800000, + y: 94.33333333333333, + }, + { + x: 1593471600000, + y: 96.43333333333334, + }, + { + x: 1593473400000, + y: 94.7, + }, + { + x: 1593475200000, + y: 93.76666666666667, + }, + { + x: 1593477000000, + y: 91.5, + }, + { + x: 1593478800000, + y: 91.9, + }, + { + x: 1593480600000, + y: 91.3, + }, + { + x: 1593482400000, + y: 98.3, + }, + { + x: 1593484200000, + y: 95.53333333333333, + }, + { + x: 1593486000000, + y: 95.66666666666667, + }, + { + x: 1593487800000, + y: 92.73333333333333, + }, + { + x: 1593489600000, + y: 93.6, + }, + { + x: 1593491400000, + y: 94.3, + }, + { + x: 1593493200000, + y: 93.13333333333334, + }, + { + x: 1593495000000, + y: 104.36666666666666, + }, + { + x: 1593496800000, + y: 107.26666666666667, + }, + { + x: 1593498600000, + y: 101.83333333333333, + }, + { + x: 1593500400000, + y: 105.46666666666667, + }, + { + x: 1593502200000, + y: 111.86666666666666, + }, + { + x: 1593504000000, + y: 111.56666666666666, + }, + { + x: 1593505800000, + y: 103.76666666666667, + }, + { + x: 1593507600000, + y: 93.9, + }, + { + x: 1593509400000, + y: 97.16666666666667, + }, + { + x: 1593511200000, + y: 93.03333333333333, + }, + { + x: 1593513000000, + y: 94.4, + }, + { + x: 1593514800000, + y: 94.76666666666667, + }, + { + x: 1593516600000, + y: 94.96666666666667, + }, + { + x: 1593518400000, + y: 101.3, + }, + { + x: 1593520200000, + y: 98.63333333333334, + }, + { + x: 1593522000000, + y: 94.8, + }, + { + x: 1593523800000, + y: 97.46666666666667, + }, + { + x: 1593525600000, + y: 95.86666666666666, + }, + { + x: 1593527400000, + y: 97.3, + }, + { + x: 1593529200000, + y: 96.1, + }, + { + x: 1593531000000, + y: 97.1, + }, + { + x: 1593532800000, + y: 97.56666666666666, + }, + { + x: 1593534600000, + y: 107.6, + }, + { + x: 1593536400000, + y: 97.46666666666667, + }, + { + x: 1593538200000, + y: 96.46666666666667, + }, + { + x: 1593540000000, + y: 93.83333333333333, + }, + { + x: 1593541800000, + y: 98.73333333333333, + }, + { + x: 1593543600000, + y: 99.86666666666666, + }, + { + x: 1593545400000, + y: 98.66666666666667, + }, + { + x: 1593547200000, + y: 102.8, + }, + { + x: 1593549000000, + y: 96.13333333333334, + }, + { + x: 1593550800000, + y: 94.53333333333333, + }, + ], + }, + 'kibana.log': { + label: 'kibana.log', + coordinates: [ + { + x: 1593295200000, + y: 11.8, + }, + { + x: 1593297000000, + y: 11.833333333333334, + }, + { + x: 1593298800000, + y: 12.1, + }, + { + x: 1593300600000, + y: 12.133333333333333, + }, + { + x: 1593302400000, + y: 11.2, + }, + { + x: 1593304200000, + y: 11.933333333333334, + }, + { + x: 1593306000000, + y: 11.466666666666667, + }, + { + x: 1593307800000, + y: 12.066666666666666, + }, + { + x: 1593309600000, + y: 11.9, + }, + { + x: 1593311400000, + y: 11.766666666666667, + }, + { + x: 1593313200000, + y: 12.066666666666666, + }, + { + x: 1593315000000, + y: 11.7, + }, + { + x: 1593316800000, + y: 11.6, + }, + { + x: 1593318600000, + y: 11.766666666666667, + }, + { + x: 1593320400000, + y: 11.633333333333333, + }, + { + x: 1593322200000, + y: 11.833333333333334, + }, + { + x: 1593324000000, + y: 11.8, + }, + { + x: 1593325800000, + y: 11.7, + }, + { + x: 1593327600000, + y: 11.666666666666666, + }, + { + x: 1593329400000, + y: 11.8, + }, + { + x: 1593331200000, + y: 11.966666666666667, + }, + { + x: 1593333000000, + y: 11.766666666666667, + }, + { + x: 1593334800000, + y: 11.766666666666667, + }, + { + x: 1593336600000, + y: 11.866666666666667, + }, + { + x: 1593338400000, + y: 11.433333333333334, + }, + { + x: 1593340200000, + y: 12.033333333333333, + }, + { + x: 1593342000000, + y: 12.1, + }, + { + x: 1593343800000, + y: 12.1, + }, + { + x: 1593345600000, + y: 11.8, + }, + { + x: 1593347400000, + y: 12.366666666666667, + }, + { + x: 1593349200000, + y: 12.033333333333333, + }, + { + x: 1593351000000, + y: 12, + }, + { + x: 1593352800000, + y: 11.8, + }, + { + x: 1593354600000, + y: 11.5, + }, + { + x: 1593356400000, + y: 12.1, + }, + { + x: 1593358200000, + y: 11.966666666666667, + }, + { + x: 1593360000000, + y: 11.9, + }, + { + x: 1593361800000, + y: 12.233333333333333, + }, + { + x: 1593363600000, + y: 11.533333333333333, + }, + { + x: 1593365400000, + y: 11.633333333333333, + }, + { + x: 1593367200000, + y: 11.866666666666667, + }, + { + x: 1593369000000, + y: 12, + }, + { + x: 1593370800000, + y: 11.7, + }, + { + x: 1593372600000, + y: 11.8, + }, + { + x: 1593374400000, + y: 11.4, + }, + { + x: 1593376200000, + y: 11.766666666666667, + }, + { + x: 1593378000000, + y: 12.033333333333333, + }, + { + x: 1593379800000, + y: 11.833333333333334, + }, + { + x: 1593381600000, + y: 11.9, + }, + { + x: 1593383400000, + y: 11.966666666666667, + }, + { + x: 1593385200000, + y: 11.8, + }, + { + x: 1593387000000, + y: 12, + }, + { + x: 1593388800000, + y: 11.933333333333334, + }, + { + x: 1593390600000, + y: 12.033333333333333, + }, + { + x: 1593392400000, + y: 12, + }, + { + x: 1593394200000, + y: 11.533333333333333, + }, + { + x: 1593396000000, + y: 11.4, + }, + { + x: 1593397800000, + y: 11.666666666666666, + }, + { + x: 1593399600000, + y: 11.633333333333333, + }, + { + x: 1593401400000, + y: 11.166666666666666, + }, + { + x: 1593403200000, + y: 11.3, + }, + { + x: 1593405000000, + y: 11.2, + }, + { + x: 1593406800000, + y: 10.966666666666667, + }, + { + x: 1593408600000, + y: 11.5, + }, + { + x: 1593410400000, + y: 11.1, + }, + { + x: 1593412200000, + y: 11.2, + }, + { + x: 1593414000000, + y: 11.4, + }, + { + x: 1593415800000, + y: 10.8, + }, + { + x: 1593417600000, + y: 11.066666666666666, + }, + { + x: 1593419400000, + y: 11.8, + }, + { + x: 1593421200000, + y: 11.266666666666667, + }, + { + x: 1593423000000, + y: 11.333333333333334, + }, + { + x: 1593424800000, + y: 11.233333333333333, + }, + { + x: 1593426600000, + y: 11.5, + }, + { + x: 1593428400000, + y: 8.2, + }, + { + x: 1593442800000, + y: 8.2, + }, + { + x: 1593444600000, + y: 11.4, + }, + { + x: 1593446400000, + y: 10.733333333333333, + }, + { + x: 1593448200000, + y: 10.833333333333334, + }, + { + x: 1593450000000, + y: 11.3, + }, + { + x: 1593451800000, + y: 11.633333333333333, + }, + { + x: 1593453600000, + y: 11.266666666666667, + }, + { + x: 1593455400000, + y: 11.3, + }, + { + x: 1593457200000, + y: 11.333333333333334, + }, + { + x: 1593459000000, + y: 11.133333333333333, + }, + { + x: 1593460800000, + y: 10.933333333333334, + }, + { + x: 1593462600000, + y: 11.2, + }, + { + x: 1593464400000, + y: 11.166666666666666, + }, + { + x: 1593466200000, + y: 11.766666666666667, + }, + { + x: 1593468000000, + y: 11.433333333333334, + }, + { + x: 1593469800000, + y: 10.8, + }, + { + x: 1593471600000, + y: 11.266666666666667, + }, + { + x: 1593473400000, + y: 11.333333333333334, + }, + { + x: 1593475200000, + y: 11.133333333333333, + }, + { + x: 1593477000000, + y: 11.133333333333333, + }, + { + x: 1593478800000, + y: 10.9, + }, + { + x: 1593480600000, + y: 11.3, + }, + { + x: 1593482400000, + y: 12.166666666666666, + }, + { + x: 1593484200000, + y: 11.433333333333334, + }, + { + x: 1593486000000, + y: 12.133333333333333, + }, + { + x: 1593487800000, + y: 11.666666666666666, + }, + { + x: 1593489600000, + y: 11.533333333333333, + }, + { + x: 1593491400000, + y: 11.833333333333334, + }, + { + x: 1593493200000, + y: 11.766666666666667, + }, + { + x: 1593495000000, + y: 11.9, + }, + { + x: 1593496800000, + y: 11.433333333333334, + }, + { + x: 1593498600000, + y: 12, + }, + { + x: 1593500400000, + y: 12.1, + }, + { + x: 1593502200000, + y: 11.6, + }, + { + x: 1593504000000, + y: 12, + }, + { + x: 1593505800000, + y: 12.233333333333333, + }, + { + x: 1593507600000, + y: 11.633333333333333, + }, + { + x: 1593509400000, + y: 11.2, + }, + { + x: 1593511200000, + y: 11.766666666666667, + }, + { + x: 1593513000000, + y: 11.9, + }, + { + x: 1593514800000, + y: 11.366666666666667, + }, + { + x: 1593516600000, + y: 11.833333333333334, + }, + { + x: 1593518400000, + y: 11.5, + }, + { + x: 1593520200000, + y: 12, + }, + { + x: 1593522000000, + y: 12.033333333333333, + }, + { + x: 1593523800000, + y: 11.733333333333333, + }, + { + x: 1593525600000, + y: 11.566666666666666, + }, + { + x: 1593527400000, + y: 11.6, + }, + { + x: 1593529200000, + y: 11.333333333333334, + }, + { + x: 1593531000000, + y: 11.833333333333334, + }, + { + x: 1593532800000, + y: 11.233333333333333, + }, + { + x: 1593534600000, + y: 11.833333333333334, + }, + { + x: 1593536400000, + y: 11.266666666666667, + }, + { + x: 1593538200000, + y: 12, + }, + { + x: 1593540000000, + y: 11.633333333333333, + }, + { + x: 1593541800000, + y: 11.9, + }, + { + x: 1593543600000, + y: 11.966666666666667, + }, + { + x: 1593545400000, + y: 11.5, + }, + { + x: 1593547200000, + y: 11.466666666666667, + }, + { + x: 1593549000000, + y: 11.4, + }, + { + x: 1593550800000, + y: 11.833333333333334, + }, + ], + }, + 'nginx.error': { + label: 'nginx.error', + coordinates: [ + { + x: 1593295200000, + y: 9.266666666666667, + }, + { + x: 1593297000000, + y: 8.833333333333334, + }, + { + x: 1593298800000, + y: 9.033333333333333, + }, + { + x: 1593300600000, + y: 8.933333333333334, + }, + { + x: 1593302400000, + y: 8.9, + }, + { + x: 1593304200000, + y: 9.6, + }, + { + x: 1593306000000, + y: 9.066666666666666, + }, + { + x: 1593307800000, + y: 8.966666666666667, + }, + { + x: 1593309600000, + y: 8.933333333333334, + }, + { + x: 1593311400000, + y: 8.5, + }, + { + x: 1593313200000, + y: 8.133333333333333, + }, + { + x: 1593315000000, + y: 8.233333333333333, + }, + { + x: 1593316800000, + y: 8.433333333333334, + }, + { + x: 1593318600000, + y: 8.4, + }, + { + x: 1593320400000, + y: 9.266666666666667, + }, + { + x: 1593322200000, + y: 8.566666666666666, + }, + { + x: 1593324000000, + y: 8.966666666666667, + }, + { + x: 1593325800000, + y: 8.833333333333334, + }, + { + x: 1593327600000, + y: 7.5, + }, + { + x: 1593329400000, + y: 8.033333333333333, + }, + { + x: 1593331200000, + y: 8.633333333333333, + }, + { + x: 1593333000000, + y: 8.5, + }, + { + x: 1593334800000, + y: 8.866666666666667, + }, + { + x: 1593336600000, + y: 8.3, + }, + { + x: 1593338400000, + y: 8.966666666666667, + }, + { + x: 1593340200000, + y: 8.2, + }, + { + x: 1593342000000, + y: 7.566666666666666, + }, + { + x: 1593343800000, + y: 7.5, + }, + { + x: 1593345600000, + y: 7.933333333333334, + }, + { + x: 1593347400000, + y: 7.866666666666666, + }, + { + x: 1593349200000, + y: 7.566666666666666, + }, + { + x: 1593351000000, + y: 7.533333333333333, + }, + { + x: 1593352800000, + y: 8.866666666666667, + }, + { + x: 1593354600000, + y: 8.566666666666666, + }, + { + x: 1593356400000, + y: 8.233333333333333, + }, + { + x: 1593358200000, + y: 8.9, + }, + { + x: 1593360000000, + y: 8.533333333333333, + }, + { + x: 1593361800000, + y: 8.733333333333333, + }, + { + x: 1593363600000, + y: 9.333333333333334, + }, + { + x: 1593365400000, + y: 9.133333333333333, + }, + { + x: 1593367200000, + y: 9.166666666666666, + }, + { + x: 1593369000000, + y: 9.266666666666667, + }, + { + x: 1593370800000, + y: 8.966666666666667, + }, + { + x: 1593372600000, + y: 9.2, + }, + { + x: 1593374400000, + y: 9.433333333333334, + }, + { + x: 1593376200000, + y: 9.166666666666666, + }, + { + x: 1593378000000, + y: 9.266666666666667, + }, + { + x: 1593379800000, + y: 9.5, + }, + { + x: 1593381600000, + y: 9.333333333333334, + }, + { + x: 1593383400000, + y: 8.8, + }, + { + x: 1593385200000, + y: 8.733333333333333, + }, + { + x: 1593387000000, + y: 8.633333333333333, + }, + { + x: 1593388800000, + y: 8.9, + }, + { + x: 1593390600000, + y: 8.533333333333333, + }, + { + x: 1593392400000, + y: 9.3, + }, + { + x: 1593394200000, + y: 9.266666666666667, + }, + { + x: 1593396000000, + y: 8.966666666666667, + }, + { + x: 1593397800000, + y: 8.666666666666666, + }, + { + x: 1593399600000, + y: 9.166666666666666, + }, + { + x: 1593401400000, + y: 8.733333333333333, + }, + { + x: 1593403200000, + y: 8.866666666666667, + }, + { + x: 1593405000000, + y: 8.633333333333333, + }, + { + x: 1593406800000, + y: 8.8, + }, + { + x: 1593408600000, + y: 8.466666666666667, + }, + { + x: 1593410400000, + y: 8.966666666666667, + }, + { + x: 1593412200000, + y: 8.166666666666666, + }, + { + x: 1593414000000, + y: 8.7, + }, + { + x: 1593415800000, + y: 8.333333333333334, + }, + { + x: 1593417600000, + y: 8.666666666666666, + }, + { + x: 1593419400000, + y: 8.533333333333333, + }, + { + x: 1593421200000, + y: 8.233333333333333, + }, + { + x: 1593423000000, + y: 8.3, + }, + { + x: 1593424800000, + y: 7.7, + }, + { + x: 1593426600000, + y: 7.7, + }, + { + x: 1593428400000, + y: 6.133333333333334, + }, + { + x: 1593430200000, + y: 0.4666666666666667, + }, + { + x: 1593442800000, + y: 7.233333333333333, + }, + { + x: 1593444600000, + y: 8.333333333333334, + }, + { + x: 1593446400000, + y: 8.666666666666666, + }, + { + x: 1593448200000, + y: 8.466666666666667, + }, + { + x: 1593450000000, + y: 8.666666666666666, + }, + { + x: 1593451800000, + y: 8.5, + }, + { + x: 1593453600000, + y: 8.6, + }, + { + x: 1593455400000, + y: 8.5, + }, + { + x: 1593457200000, + y: 8.6, + }, + { + x: 1593459000000, + y: 8.866666666666667, + }, + { + x: 1593460800000, + y: 9.166666666666666, + }, + { + x: 1593462600000, + y: 8.4, + }, + { + x: 1593464400000, + y: 8.533333333333333, + }, + { + x: 1593466200000, + y: 8.066666666666666, + }, + { + x: 1593468000000, + y: 8.666666666666666, + }, + { + x: 1593469800000, + y: 8.966666666666667, + }, + { + x: 1593471600000, + y: 8.4, + }, + { + x: 1593473400000, + y: 8.833333333333334, + }, + { + x: 1593475200000, + y: 8.533333333333333, + }, + { + x: 1593477000000, + y: 8.066666666666666, + }, + { + x: 1593478800000, + y: 8.533333333333333, + }, + { + x: 1593480600000, + y: 8.633333333333333, + }, + { + x: 1593482400000, + y: 8.933333333333334, + }, + { + x: 1593484200000, + y: 8.833333333333334, + }, + { + x: 1593486000000, + y: 8.4, + }, + { + x: 1593487800000, + y: 8.633333333333333, + }, + { + x: 1593489600000, + y: 9.333333333333334, + }, + { + x: 1593491400000, + y: 9.366666666666667, + }, + { + x: 1593493200000, + y: 8.333333333333334, + }, + { + x: 1593495000000, + y: 9.266666666666667, + }, + { + x: 1593496800000, + y: 8.2, + }, + { + x: 1593498600000, + y: 8.4, + }, + { + x: 1593500400000, + y: 8.433333333333334, + }, + { + x: 1593502200000, + y: 7.633333333333334, + }, + { + x: 1593504000000, + y: 7.766666666666667, + }, + { + x: 1593505800000, + y: 8.4, + }, + { + x: 1593507600000, + y: 8.3, + }, + { + x: 1593509400000, + y: 8.833333333333334, + }, + { + x: 1593511200000, + y: 8.433333333333334, + }, + { + x: 1593513000000, + y: 8.766666666666667, + }, + { + x: 1593514800000, + y: 9.066666666666666, + }, + { + x: 1593516600000, + y: 8.4, + }, + { + x: 1593518400000, + y: 8.4, + }, + { + x: 1593520200000, + y: 8.8, + }, + { + x: 1593522000000, + y: 8.466666666666667, + }, + { + x: 1593523800000, + y: 8.633333333333333, + }, + { + x: 1593525600000, + y: 9.133333333333333, + }, + { + x: 1593527400000, + y: 8.7, + }, + { + x: 1593529200000, + y: 8.566666666666666, + }, + { + x: 1593531000000, + y: 9.033333333333333, + }, + { + x: 1593532800000, + y: 8.9, + }, + { + x: 1593534600000, + y: 8.7, + }, + { + x: 1593536400000, + y: 8.7, + }, + { + x: 1593538200000, + y: 8.8, + }, + { + x: 1593540000000, + y: 9.166666666666666, + }, + { + x: 1593541800000, + y: 9.033333333333333, + }, + { + x: 1593543600000, + y: 8.733333333333333, + }, + { + x: 1593545400000, + y: 9.2, + }, + { + x: 1593547200000, + y: 8.933333333333334, + }, + { + x: 1593549000000, + y: 9.2, + }, + { + x: 1593550800000, + y: 9.333333333333334, + }, + ], + }, + sample_web_logs: { + label: 'sample_web_logs', + coordinates: [ + { + x: 1593430200000, + y: 0.5666666666666667, + }, + { + x: 1593432000000, + y: 0.36666666666666664, + }, + { + x: 1593433800000, + y: 0.5666666666666667, + }, + { + x: 1593435600000, + y: 0.4666666666666667, + }, + { + x: 1593437400000, + y: 0.36666666666666664, + }, + { + x: 1593439200000, + y: 0.3, + }, + { + x: 1593441000000, + y: 0.13333333333333333, + }, + ], + }, + 'postgresql.log': { + label: 'postgresql.log', + coordinates: [ + { + x: 1593439200000, + y: 0.1, + }, + { + x: 1593441000000, + y: 0.1, + }, + ], + }, + }, +}; + +export const emptyResponse: LogsFetchDataResponse = { + title: 'Logs', + appLink: '/app/logs', + stats: {}, + series: {}, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts new file mode 100644 index 0000000000000..37233b4f6342c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.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 { MetricsFetchDataResponse, FetchData } from '../../../typings'; + +export const fetchMetricsData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: MetricsFetchDataResponse = { + title: 'Metrics', + appLink: '/app/apm', + stats: { + hosts: { value: 11, type: 'number' }, + cpu: { value: 0.8, type: 'percent' }, + memory: { value: 0.362, type: 'percent' }, + inboundTraffic: { value: 1024, type: 'bytesPerSecond' }, + outboundTraffic: { value: 1024, type: 'bytesPerSecond' }, + }, + series: { + outboundTraffic: { + coordinates: [ + { + x: 1589805437549, + y: 331514, + }, + { + x: 1590047357549, + y: 319208, + }, + { + x: 1590289277549, + y: 309648, + }, + { + x: 1590531197549, + y: 280568, + }, + { + x: 1590773117549, + y: 337180, + }, + { + x: 1591015037549, + y: 122468, + }, + { + x: 1591256957549, + y: 184164, + }, + { + x: 1591498877549, + y: 316323, + }, + { + x: 1591740797549, + y: 307351, + }, + { + x: 1591982717549, + y: 290262, + }, + ], + }, + inboundTraffic: { + coordinates: [ + { + x: 1589805437549, + y: 331514, + }, + { + x: 1590047357549, + y: 319208, + }, + { + x: 1590289277549, + y: 309648, + }, + { + x: 1590531197549, + y: 280568, + }, + { + x: 1590773117549, + y: 337180, + }, + { + x: 1591015037549, + y: 122468, + }, + { + x: 1591256957549, + y: 184164, + }, + { + x: 1591498877549, + y: 316323, + }, + { + x: 1591740797549, + y: 307351, + }, + { + x: 1591982717549, + y: 290262, + }, + ], + }, + }, +}; + +export const emptyResponse: MetricsFetchDataResponse = { + title: 'Metrics', + appLink: '/app/apm', + stats: { + hosts: { value: 0, type: 'number' }, + cpu: { value: 0, type: 'percent' }, + memory: { value: 0, type: 'percent' }, + inboundTraffic: { value: 0, type: 'bytesPerSecond' }, + outboundTraffic: { value: 0, type: 'bytesPerSecond' }, + }, + series: { + outboundTraffic: { + coordinates: [], + }, + inboundTraffic: { + coordinates: [], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/news_feed.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/news_feed.mock.ts new file mode 100644 index 0000000000000..b23d095e2775b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/news_feed.mock.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. + */ + +export const newsFeedFetchData = async () => { + return { + items: [ + { + title: { + en: 'Elastic introduces OpenTelemetry integration', + }, + description: { + en: + 'We are pleased to announce the availability of the Elastic OpenTelemetry integration — available on Elastic Cloud, or when you download Elastic APM.', + }, + link_text: null, + link_url: { + en: + 'https://www.elastic.co/blog/elastic-apm-opentelemetry-integration?blade=observabilitysolutionfeed', + }, + languages: null, + badge: null, + image_url: null, + publish_on: '2020-07-02T00:00:00', + expire_on: '2021-05-02T00:00:00', + hash: '012caf3e161127d618ae8cc95e3e63f009a45d343eedf2f5e369cc95b1f9d9d3', + }, + { + title: { + en: 'Kubernetes observability tutorial: Log monitoring and analysis', + }, + description: { + en: + 'Learn how Elastic Observability makes it easy to monitor and detect anomalies in millions of logs from thousands of containers running hundreds of microservices — while Kubernetes scales applications with changing pod counts. All from a single UI.', + }, + link_text: null, + link_url: { + en: + 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-log-monitoring-and-analysis-elastic-stack?blade=observabilitysolutionfeed', + }, + languages: null, + badge: null, + image_url: null, + publish_on: '2020-06-23T00:00:00', + expire_on: '2021-06-23T00:00:00', + hash: '79a28cb9be717e82df80bf32c27e5d475e56d0d315be694b661d133f9a58b3b3', + }, + { + title: { + en: 'Kubernetes observability tutorial: K8s cluster setup and demo app deployment', + }, + description: { + en: + 'This blog will walk you through configuring the environment you will be using for the Kubernetes observability tutorial blog series. We will be deploying Elasticsearch Service, a Minikube single-node Kubernetes cluster setup, and a demo app.', + }, + link_text: null, + link_url: { + en: + 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-cluster-setup-demo-app-deployment?blade=observabilitysolutionfeed', + }, + languages: null, + badge: null, + image_url: null, + publish_on: '2020-06-23T00:00:00', + expire_on: '2021-06-23T00:00:00', + hash: 'ad682c355af3d4470a14df116df3b441e941661b291cdac62335615e7c6f13c2', + }, + ], + }; +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts new file mode 100644 index 0000000000000..ab5874f8bfcd4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts @@ -0,0 +1,1218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { UptimeFetchDataResponse, FetchData } from '../../../typings'; + +export const fetchUptimeData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: UptimeFetchDataResponse = { + title: 'Uptime', + appLink: '/app/uptime#/', + stats: { + monitors: { + type: 'number', + value: 26, + }, + up: { + type: 'number', + value: 20, + }, + down: { + type: 'number', + value: 6, + }, + }, + series: { + up: { + coordinates: [ + { + x: 1593295200000, + y: 1170, + }, + { + x: 1593297000000, + y: 1170, + }, + { + x: 1593298800000, + y: 1170, + }, + { + x: 1593300600000, + y: 1170, + }, + { + x: 1593302400000, + y: 1170, + }, + { + x: 1593304200000, + y: 1170, + }, + { + x: 1593306000000, + y: 1170, + }, + { + x: 1593307800000, + y: 1170, + }, + { + x: 1593309600000, + y: 1170, + }, + { + x: 1593311400000, + y: 1170, + }, + { + x: 1593313200000, + y: 1170, + }, + { + x: 1593315000000, + y: 1170, + }, + { + x: 1593316800000, + y: 1170, + }, + { + x: 1593318600000, + y: 1170, + }, + { + x: 1593320400000, + y: 1170, + }, + { + x: 1593322200000, + y: 1170, + }, + { + x: 1593324000000, + y: 1170, + }, + { + x: 1593325800000, + y: 1170, + }, + { + x: 1593327600000, + y: 1170, + }, + { + x: 1593329400000, + y: 1170, + }, + { + x: 1593331200000, + y: 1170, + }, + { + x: 1593333000000, + y: 1170, + }, + { + x: 1593334800000, + y: 1170, + }, + { + x: 1593336600000, + y: 1170, + }, + { + x: 1593338400000, + y: 1170, + }, + { + x: 1593340200000, + y: 1170, + }, + { + x: 1593342000000, + y: 1170, + }, + { + x: 1593343800000, + y: 1170, + }, + { + x: 1593345600000, + y: 1170, + }, + { + x: 1593347400000, + y: 1170, + }, + { + x: 1593349200000, + y: 1170, + }, + { + x: 1593351000000, + y: 1170, + }, + { + x: 1593352800000, + y: 1170, + }, + { + x: 1593354600000, + y: 1170, + }, + { + x: 1593356400000, + y: 1170, + }, + { + x: 1593358200000, + y: 1170, + }, + { + x: 1593360000000, + y: 1170, + }, + { + x: 1593361800000, + y: 1170, + }, + { + x: 1593363600000, + y: 1170, + }, + { + x: 1593365400000, + y: 1170, + }, + { + x: 1593367200000, + y: 1170, + }, + { + x: 1593369000000, + y: 1170, + }, + { + x: 1593370800000, + y: 1170, + }, + { + x: 1593372600000, + y: 1170, + }, + { + x: 1593374400000, + y: 1169, + }, + { + x: 1593376200000, + y: 1170, + }, + { + x: 1593378000000, + y: 1170, + }, + { + x: 1593379800000, + y: 1170, + }, + { + x: 1593381600000, + y: 1170, + }, + { + x: 1593383400000, + y: 1170, + }, + { + x: 1593385200000, + y: 1170, + }, + { + x: 1593387000000, + y: 1170, + }, + { + x: 1593388800000, + y: 1170, + }, + { + x: 1593390600000, + y: 1170, + }, + { + x: 1593392400000, + y: 1170, + }, + { + x: 1593394200000, + y: 1239, + }, + { + x: 1593396000000, + y: 1170, + }, + { + x: 1593397800000, + y: 1170, + }, + { + x: 1593399600000, + y: 1170, + }, + { + x: 1593401400000, + y: 1170, + }, + { + x: 1593403200000, + y: 1170, + }, + { + x: 1593405000000, + y: 1170, + }, + { + x: 1593406800000, + y: 1170, + }, + { + x: 1593408600000, + y: 1170, + }, + { + x: 1593410400000, + y: 1170, + }, + { + x: 1593412200000, + y: 1170, + }, + { + x: 1593414000000, + y: 1170, + }, + { + x: 1593415800000, + y: 1170, + }, + { + x: 1593417600000, + y: 1170, + }, + { + x: 1593419400000, + y: 1170, + }, + { + x: 1593421200000, + y: 1170, + }, + { + x: 1593423000000, + y: 1170, + }, + { + x: 1593424800000, + y: 1166, + }, + { + x: 1593426600000, + y: 1206, + }, + { + x: 1593428400000, + y: 1143, + }, + { + x: 1593430200000, + y: 1170, + }, + { + x: 1593432000000, + y: 1170, + }, + { + x: 1593433800000, + y: 1170, + }, + { + x: 1593435600000, + y: 1170, + }, + { + x: 1593437400000, + y: 1170, + }, + { + x: 1593439200000, + y: 1170, + }, + { + x: 1593441000000, + y: 1170, + }, + { + x: 1593442800000, + y: 1170, + }, + { + x: 1593444600000, + y: 1170, + }, + { + x: 1593446400000, + y: 1170, + }, + { + x: 1593448200000, + y: 1170, + }, + { + x: 1593450000000, + y: 1170, + }, + { + x: 1593451800000, + y: 1170, + }, + { + x: 1593453600000, + y: 1170, + }, + { + x: 1593455400000, + y: 1170, + }, + { + x: 1593457200000, + y: 1170, + }, + { + x: 1593459000000, + y: 1170, + }, + { + x: 1593460800000, + y: 1170, + }, + { + x: 1593462600000, + y: 1170, + }, + { + x: 1593464400000, + y: 1170, + }, + { + x: 1593466200000, + y: 1170, + }, + { + x: 1593468000000, + y: 1170, + }, + { + x: 1593469800000, + y: 1170, + }, + { + x: 1593471600000, + y: 1170, + }, + { + x: 1593473400000, + y: 1170, + }, + { + x: 1593475200000, + y: 1170, + }, + { + x: 1593477000000, + y: 1170, + }, + { + x: 1593478800000, + y: 1170, + }, + { + x: 1593480600000, + y: 1201, + }, + { + x: 1593482400000, + y: 1139, + }, + { + x: 1593484200000, + y: 1140, + }, + { + x: 1593486000000, + y: 1140, + }, + { + x: 1593487800000, + y: 1140, + }, + { + x: 1593489600000, + y: 1140, + }, + { + x: 1593491400000, + y: 1140, + }, + { + x: 1593493200000, + y: 1140, + }, + { + x: 1593495000000, + y: 1140, + }, + { + x: 1593496800000, + y: 1140, + }, + { + x: 1593498600000, + y: 1140, + }, + { + x: 1593500400000, + y: 1140, + }, + { + x: 1593502200000, + y: 1140, + }, + { + x: 1593504000000, + y: 1140, + }, + { + x: 1593505800000, + y: 1140, + }, + { + x: 1593507600000, + y: 1140, + }, + { + x: 1593509400000, + y: 1140, + }, + { + x: 1593511200000, + y: 1140, + }, + { + x: 1593513000000, + y: 1140, + }, + { + x: 1593514800000, + y: 1140, + }, + { + x: 1593516600000, + y: 1140, + }, + { + x: 1593518400000, + y: 1140, + }, + { + x: 1593520200000, + y: 1140, + }, + { + x: 1593522000000, + y: 1140, + }, + { + x: 1593523800000, + y: 1140, + }, + { + x: 1593525600000, + y: 1140, + }, + { + x: 1593527400000, + y: 1140, + }, + { + x: 1593529200000, + y: 1140, + }, + { + x: 1593531000000, + y: 1140, + }, + { + x: 1593532800000, + y: 1140, + }, + { + x: 1593534600000, + y: 1140, + }, + { + x: 1593536400000, + y: 1140, + }, + { + x: 1593538200000, + y: 1140, + }, + { + x: 1593540000000, + y: 1140, + }, + { + x: 1593541800000, + y: 1139, + }, + { + x: 1593543600000, + y: 1140, + }, + { + x: 1593545400000, + y: 1140, + }, + { + x: 1593547200000, + y: 1140, + }, + { + x: 1593549000000, + y: 1140, + }, + { + x: 1593550800000, + y: 1140, + }, + { + x: 1593552600000, + y: 1140, + }, + ], + }, + down: { + coordinates: [ + { + x: 1593295200000, + y: 234, + }, + { + x: 1593297000000, + y: 234, + }, + { + x: 1593298800000, + y: 234, + }, + { + x: 1593300600000, + y: 234, + }, + { + x: 1593302400000, + y: 234, + }, + { + x: 1593304200000, + y: 234, + }, + { + x: 1593306000000, + y: 234, + }, + { + x: 1593307800000, + y: 234, + }, + { + x: 1593309600000, + y: 234, + }, + { + x: 1593311400000, + y: 234, + }, + { + x: 1593313200000, + y: 234, + }, + { + x: 1593315000000, + y: 234, + }, + { + x: 1593316800000, + y: 234, + }, + { + x: 1593318600000, + y: 234, + }, + { + x: 1593320400000, + y: 234, + }, + { + x: 1593322200000, + y: 234, + }, + { + x: 1593324000000, + y: 234, + }, + { + x: 1593325800000, + y: 234, + }, + { + x: 1593327600000, + y: 234, + }, + { + x: 1593329400000, + y: 234, + }, + { + x: 1593331200000, + y: 234, + }, + { + x: 1593333000000, + y: 234, + }, + { + x: 1593334800000, + y: 234, + }, + { + x: 1593336600000, + y: 234, + }, + { + x: 1593338400000, + y: 234, + }, + { + x: 1593340200000, + y: 234, + }, + { + x: 1593342000000, + y: 234, + }, + { + x: 1593343800000, + y: 234, + }, + { + x: 1593345600000, + y: 234, + }, + { + x: 1593347400000, + y: 234, + }, + { + x: 1593349200000, + y: 234, + }, + { + x: 1593351000000, + y: 234, + }, + { + x: 1593352800000, + y: 234, + }, + { + x: 1593354600000, + y: 234, + }, + { + x: 1593356400000, + y: 234, + }, + { + x: 1593358200000, + y: 234, + }, + { + x: 1593360000000, + y: 234, + }, + { + x: 1593361800000, + y: 234, + }, + { + x: 1593363600000, + y: 234, + }, + { + x: 1593365400000, + y: 234, + }, + { + x: 1593367200000, + y: 234, + }, + { + x: 1593369000000, + y: 234, + }, + { + x: 1593370800000, + y: 234, + }, + { + x: 1593372600000, + y: 234, + }, + { + x: 1593374400000, + y: 235, + }, + { + x: 1593376200000, + y: 234, + }, + { + x: 1593378000000, + y: 234, + }, + { + x: 1593379800000, + y: 234, + }, + { + x: 1593381600000, + y: 234, + }, + { + x: 1593383400000, + y: 234, + }, + { + x: 1593385200000, + y: 234, + }, + { + x: 1593387000000, + y: 234, + }, + { + x: 1593388800000, + y: 234, + }, + { + x: 1593390600000, + y: 234, + }, + { + x: 1593392400000, + y: 234, + }, + { + x: 1593394200000, + y: 246, + }, + { + x: 1593396000000, + y: 234, + }, + { + x: 1593397800000, + y: 234, + }, + { + x: 1593399600000, + y: 234, + }, + { + x: 1593401400000, + y: 234, + }, + { + x: 1593403200000, + y: 234, + }, + { + x: 1593405000000, + y: 234, + }, + { + x: 1593406800000, + y: 234, + }, + { + x: 1593408600000, + y: 234, + }, + { + x: 1593410400000, + y: 234, + }, + { + x: 1593412200000, + y: 234, + }, + { + x: 1593414000000, + y: 234, + }, + { + x: 1593415800000, + y: 234, + }, + { + x: 1593417600000, + y: 234, + }, + { + x: 1593419400000, + y: 234, + }, + { + x: 1593421200000, + y: 234, + }, + { + x: 1593423000000, + y: 234, + }, + { + x: 1593424800000, + y: 240, + }, + { + x: 1593426600000, + y: 254, + }, + { + x: 1593428400000, + y: 231, + }, + { + x: 1593430200000, + y: 234, + }, + { + x: 1593432000000, + y: 234, + }, + { + x: 1593433800000, + y: 234, + }, + { + x: 1593435600000, + y: 234, + }, + { + x: 1593437400000, + y: 234, + }, + { + x: 1593439200000, + y: 234, + }, + { + x: 1593441000000, + y: 234, + }, + { + x: 1593442800000, + y: 234, + }, + { + x: 1593444600000, + y: 234, + }, + { + x: 1593446400000, + y: 234, + }, + { + x: 1593448200000, + y: 234, + }, + { + x: 1593450000000, + y: 234, + }, + { + x: 1593451800000, + y: 234, + }, + { + x: 1593453600000, + y: 234, + }, + { + x: 1593455400000, + y: 234, + }, + { + x: 1593457200000, + y: 234, + }, + { + x: 1593459000000, + y: 234, + }, + { + x: 1593460800000, + y: 234, + }, + { + x: 1593462600000, + y: 234, + }, + { + x: 1593464400000, + y: 234, + }, + { + x: 1593466200000, + y: 234, + }, + { + x: 1593468000000, + y: 234, + }, + { + x: 1593469800000, + y: 234, + }, + { + x: 1593471600000, + y: 234, + }, + { + x: 1593473400000, + y: 234, + }, + { + x: 1593475200000, + y: 234, + }, + { + x: 1593477000000, + y: 234, + }, + { + x: 1593478800000, + y: 234, + }, + { + x: 1593480600000, + y: 254, + }, + { + x: 1593482400000, + y: 265, + }, + { + x: 1593484200000, + y: 264, + }, + { + x: 1593486000000, + y: 264, + }, + { + x: 1593487800000, + y: 264, + }, + { + x: 1593489600000, + y: 264, + }, + { + x: 1593491400000, + y: 264, + }, + { + x: 1593493200000, + y: 264, + }, + { + x: 1593495000000, + y: 264, + }, + { + x: 1593496800000, + y: 264, + }, + { + x: 1593498600000, + y: 264, + }, + { + x: 1593500400000, + y: 264, + }, + { + x: 1593502200000, + y: 264, + }, + { + x: 1593504000000, + y: 264, + }, + { + x: 1593505800000, + y: 264, + }, + { + x: 1593507600000, + y: 264, + }, + { + x: 1593509400000, + y: 264, + }, + { + x: 1593511200000, + y: 264, + }, + { + x: 1593513000000, + y: 264, + }, + { + x: 1593514800000, + y: 264, + }, + { + x: 1593516600000, + y: 264, + }, + { + x: 1593518400000, + y: 264, + }, + { + x: 1593520200000, + y: 264, + }, + { + x: 1593522000000, + y: 264, + }, + { + x: 1593523800000, + y: 264, + }, + { + x: 1593525600000, + y: 264, + }, + { + x: 1593527400000, + y: 264, + }, + { + x: 1593529200000, + y: 264, + }, + { + x: 1593531000000, + y: 264, + }, + { + x: 1593532800000, + y: 264, + }, + { + x: 1593534600000, + y: 264, + }, + { + x: 1593536400000, + y: 264, + }, + { + x: 1593538200000, + y: 264, + }, + { + x: 1593540000000, + y: 264, + }, + { + x: 1593541800000, + y: 265, + }, + { + x: 1593543600000, + y: 264, + }, + { + x: 1593545400000, + y: 264, + }, + { + x: 1593547200000, + y: 264, + }, + { + x: 1593549000000, + y: 264, + }, + { + x: 1593550800000, + y: 264, + }, + { + x: 1593552600000, + y: 264, + }, + ], + }, + }, +}; + +export const emptyResponse: UptimeFetchDataResponse = { + title: 'Uptime', + appLink: '/app/uptime#/', + stats: { + monitors: { + type: 'number', + value: 0, + }, + up: { + type: 'number', + value: 0, + }, + down: { + type: 'number', + value: 0, + }, + }, + series: { + up: { + coordinates: [], + }, + down: { + coordinates: [], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx new file mode 100644 index 0000000000000..896cad7b72ecd --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -0,0 +1,582 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { AppMountContext } from 'kibana/public'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; +import { PluginContext } from '../../context/plugin_context'; +import { registerDataHandler, unregisterDataHandler } from '../../data_handler'; +import { emptyResponse as emptyAPMResponse, fetchApmData } from './mock/apm.mock'; +import { fetchLogsData, emptyResponse as emptyLogsResponse } from './mock/logs.mock'; +import { fetchMetricsData, emptyResponse as emptyMetricsResponse } from './mock/metrics.mock'; +import { fetchUptimeData, emptyResponse as emptyUptimeResponse } from './mock/uptime.mock'; +import { EuiThemeProvider } from '../../typings'; +import { OverviewPage } from './'; +import { alertsFetchData } from './mock/alerts.mock'; +import { newsFeedFetchData } from './mock/news_feed.mock'; + +const core = { + http: { + basePath: { + prepend: (link) => `http://localhost:5601${link}`, + }, + }, + uiSettings: { + get: (key: string) => { + const euiSettings = { + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + from: 'now-15m', + to: 'now', + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + pause: true, + value: 1000, + }, + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + }; + // @ts-expect-error + return euiSettings[key]; + }, + }, +} as AppMountContext['core']; + +const coreWithAlerts = ({ + ...core, + http: { + ...core.http, + get: alertsFetchData, + }, +} as unknown) as AppMountContext['core']; + +const coreWithNewsFeed = ({ + ...core, + http: { + ...core.http, + get: newsFeedFetchData, + }, +} as unknown) as AppMountContext['core']; + +function unregisterAll() { + unregisterDataHandler({ appName: 'apm' }); + unregisterDataHandler({ appName: 'infra_logs' }); + unregisterDataHandler({ appName: 'infra_metrics' }); + unregisterDataHandler({ appName: 'uptime' }); +} + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('Empty state', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => false, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => false, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => false, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => false, + }); + + return ; + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('single panel', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs and metrics', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics and alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM and alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM and Uptime', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM, Uptime and Alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM, Uptime and News feed', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('no data', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: async () => emptyAPMResponse, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: async () => emptyLogsResponse, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: async () => emptyMetricsResponse, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: async () => emptyUptimeResponse, + hasData: async () => true, + }); + return ( + + ); + }); + +const coreAlertsThrowsError = ({ + ...core, + http: { + ...core.http, + get: async () => { + throw new Error('Error fetching Alerts data'); + }, + }, +} as unknown) as AppMountContext['core']; +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('fetch data with error', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: async () => { + throw new Error('Error fetching APM data'); + }, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: async () => { + throw new Error('Error fetching Logs data'); + }, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: async () => { + throw new Error('Error fetching Metric data'); + }, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: async () => { + throw new Error('Error fetching Uptime data'); + }, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('hasData with error and alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + return ( + + ); + }); +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('hasData with error', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + return ( + + ); + }); diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx new file mode 100644 index 0000000000000..10f9b4dc42723 --- /dev/null +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 * as t from 'io-ts'; +import { i18n } from '@kbn/i18n'; +import { HomePage } from '../pages/home'; +import { LandingPage } from '../pages/landing'; +import { OverviewPage } from '../pages/overview'; +import { jsonRt } from './json_rt'; + +export type RouteParams = DecodeParams; + +type DecodeParams = { + [key in keyof TParams]: TParams[key] extends t.Any ? t.TypeOf : never; +}; + +export interface Params { + query?: t.HasProps; + path?: t.HasProps; +} +export const routes = { + '/': { + handler: () => { + return ; + }, + params: {}, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.home.breadcrumb', { + defaultMessage: 'Overview', + }), + }, + ], + }, + '/landing': { + handler: () => { + return ; + }, + params: {}, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.landing.breadcrumb', { + defaultMessage: 'Getting started', + }), + }, + ], + }, + '/overview': { + handler: ({ query }: any) => { + return ; + }, + params: { + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + refreshPaused: jsonRt.pipe(t.boolean), + refreshInterval: jsonRt.pipe(t.number), + }), + }, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.overview.breadcrumb', { + defaultMessage: 'Overview', + }), + }, + ], + }, +}; diff --git a/x-pack/plugins/observability/public/routes/json_rt.ts b/x-pack/plugins/observability/public/routes/json_rt.ts new file mode 100644 index 0000000000000..fcc73547a686b --- /dev/null +++ b/x-pack/plugins/observability/public/routes/json_rt.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 * as t from 'io-ts'; +import { either } from 'fp-ts/lib/Either'; + +export const jsonRt = new t.Type( + 'JSON', + t.any.is, + (input, context) => + either.chain(t.string.validate(input, context), (str) => { + try { + return t.success(JSON.parse(str)); + } catch (e) { + return t.failure(input, context); + } + }), + (a) => JSON.stringify(a) +); diff --git a/x-pack/plugins/observability/public/services/get_news_feed.test.ts b/x-pack/plugins/observability/public/services/get_news_feed.test.ts new file mode 100644 index 0000000000000..49eb2da803ab6 --- /dev/null +++ b/x-pack/plugins/observability/public/services/get_news_feed.test.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 { getNewsFeed } from './get_news_feed'; +import { AppMountContext } from 'kibana/public'; + +describe('getNewsFeed', () => { + it('Returns empty array when api throws exception', async () => { + const core = ({ + http: { + get: async () => { + throw new Error('Boom'); + }, + }, + } as unknown) as AppMountContext['core']; + + const newsFeed = await getNewsFeed({ core }); + expect(newsFeed.items).toEqual([]); + }); + it('Returns array with the news feed', async () => { + const core = ({ + http: { + get: async () => { + return { + items: [ + { + title: { + en: 'Elastic introduces OpenTelemetry integration', + }, + description: { + en: + 'We are pleased to announce the availability of the Elastic OpenTelemetry integration — available on Elastic Cloud, or when you download Elastic APM.', + }, + link_text: null, + link_url: { + en: + 'https://www.elastic.co/blog/elastic-apm-opentelemetry-integration?blade=observabilitysolutionfeed', + }, + languages: null, + badge: null, + image_url: null, + publish_on: '2020-07-02T00:00:00', + expire_on: '2021-05-02T00:00:00', + hash: '012caf3e161127d618ae8cc95e3e63f009a45d343eedf2f5e369cc95b1f9d9d3', + }, + { + title: { + en: 'Kubernetes observability tutorial: Log monitoring and analysis', + }, + description: { + en: + 'Learn how Elastic Observability makes it easy to monitor and detect anomalies in millions of logs from thousands of containers running hundreds of microservices — while Kubernetes scales applications with changing pod counts. All from a single UI.', + }, + link_text: null, + link_url: { + en: + 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-log-monitoring-and-analysis-elastic-stack?blade=observabilitysolutionfeed', + }, + languages: null, + badge: null, + image_url: null, + publish_on: '2020-06-23T00:00:00', + expire_on: '2021-06-23T00:00:00', + hash: '79a28cb9be717e82df80bf32c27e5d475e56d0d315be694b661d133f9a58b3b3', + }, + { + title: { + en: + 'Kubernetes observability tutorial: K8s cluster setup and demo app deployment', + }, + description: { + en: + 'This blog will walk you through configuring the environment you will be using for the Kubernetes observability tutorial blog series. We will be deploying Elasticsearch Service, a Minikube single-node Kubernetes cluster setup, and a demo app.', + }, + link_text: null, + link_url: { + en: + 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-cluster-setup-demo-app-deployment?blade=observabilitysolutionfeed', + }, + languages: null, + badge: null, + image_url: null, + publish_on: '2020-06-23T00:00:00', + expire_on: '2021-06-23T00:00:00', + hash: 'ad682c355af3d4470a14df116df3b441e941661b291cdac62335615e7c6f13c2', + }, + ], + }; + }, + }, + } as unknown) as AppMountContext['core']; + + const newsFeed = await getNewsFeed({ core }); + expect(newsFeed.items.length).toEqual(3); + }); +}); diff --git a/x-pack/plugins/observability/public/services/get_news_feed.ts b/x-pack/plugins/observability/public/services/get_news_feed.ts new file mode 100644 index 0000000000000..3a6e60fa74188 --- /dev/null +++ b/x-pack/plugins/observability/public/services/get_news_feed.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AppMountContext } from 'kibana/public'; + +export interface NewsItem { + title: { en: string }; + description: { en: string }; + link_url: { en: string }; + image_url?: { en: string } | null; +} + +interface NewsFeed { + items: NewsItem[]; +} + +export async function getNewsFeed({ core }: { core: AppMountContext['core'] }): Promise { + try { + return await core.http.get('https://feeds.elastic.co/observability-solution/v8.0.0.json'); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error while fetching news feed', e); + return { items: [] }; + } +} diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts new file mode 100644 index 0000000000000..dd3f476fe7d53 --- /dev/null +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.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 { AppMountContext } from 'kibana/public'; +import { getObservabilityAlerts } from './get_observability_alerts'; + +describe('getObservabilityAlerts', () => { + it('Returns empty array when api throws exception', async () => { + const core = ({ + http: { + get: async () => { + throw new Error('Boom'); + }, + }, + } as unknown) as AppMountContext['core']; + + const alerts = await getObservabilityAlerts({ core }); + expect(alerts).toEqual([]); + }); + + it('Returns empty array when api return undefined', async () => { + const core = ({ + http: { + get: async () => { + return { + data: undefined, + }; + }, + }, + } as unknown) as AppMountContext['core']; + + const alerts = await getObservabilityAlerts({ core }); + expect(alerts).toEqual([]); + }); + + it('Shows alerts from Observability', async () => { + const core = ({ + http: { + get: async () => { + return { + data: [ + { + id: 1, + consumer: 'siem', + }, + { + id: 2, + consumer: 'apm', + }, + { + id: 3, + consumer: 'uptime', + }, + { + id: 4, + consumer: 'logs', + }, + { + id: 5, + consumer: 'metrics', + }, + ], + }; + }, + }, + } as unknown) as AppMountContext['core']; + + const alerts = await getObservabilityAlerts({ core }); + expect(alerts).toEqual([ + { + id: 2, + consumer: 'apm', + }, + { + id: 3, + consumer: 'uptime', + }, + { + id: 4, + consumer: 'logs', + }, + { + id: 5, + consumer: 'metrics', + }, + ]); + }); +}); diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts new file mode 100644 index 0000000000000..49855a30c16f6 --- /dev/null +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AppMountContext } from 'kibana/public'; +import { Alert } from '../../../alerts/common'; + +export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) { + try { + const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', { + query: { + page: 1, + per_page: 20, + }, + }); + + return data.filter(({ consumer }) => { + return ( + consumer === 'apm' || consumer === 'uptime' || consumer === 'logs' || consumer === 'metrics' + ); + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error while fetching alerts', e); + return []; + } +} diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index e57dfebb36419..2dafd70896cc5 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -6,11 +6,9 @@ import { ObservabilityApp } from '../../../typings/common'; -interface Stat { +export interface Stat { type: 'number' | 'percent' | 'bytesPerSecond'; - label: string; value: number; - color?: string; } export interface Coordinates { @@ -18,10 +16,8 @@ export interface Coordinates { y?: number; } -interface Series { - label: string; +export interface Series { coordinates: Coordinates[]; - color?: string; } export interface FetchDataParams { @@ -50,8 +46,8 @@ export interface FetchDataResponse { } export interface LogsFetchDataResponse extends FetchDataResponse { - stats: Record; - series: Record; + stats: Record; + series: Record; } export interface MetricsFetchDataResponse extends FetchDataResponse { diff --git a/x-pack/plugins/observability/public/typings/section/index.ts b/x-pack/plugins/observability/public/typings/section/index.ts new file mode 100644 index 0000000000000..f336b6b981687 --- /dev/null +++ b/x-pack/plugins/observability/public/typings/section/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ObservabilityApp } from '../../../typings/common'; + +export interface ISection { + id: ObservabilityApp | 'alert'; + title: string; + icon: string; + description: string; + href?: string; + linkTitle?: string; + target?: '_blank'; +} diff --git a/x-pack/plugins/observability/public/utils/date.ts b/x-pack/plugins/observability/public/utils/date.ts new file mode 100644 index 0000000000000..fc0bbdae20cb9 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/date.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import datemath from '@elastic/datemath'; + +export function getParsedDate(range?: string, opts = {}) { + if (range) { + const parsed = datemath.parse(range, opts); + if (parsed) { + return parsed.toISOString(); + } + } +} diff --git a/x-pack/plugins/observability/public/utils/format_stat_value.test.ts b/x-pack/plugins/observability/public/utils/format_stat_value.test.ts new file mode 100644 index 0000000000000..6643692e02dd4 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/format_stat_value.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 { formatStatValue } from './format_stat_value'; +import { Stat } from '../typings'; + +describe('formatStatValue', () => { + it('formats value as number', () => { + const stat = { + type: 'number', + label: 'numeral stat', + value: 1000, + } as Stat; + expect(formatStatValue(stat)).toEqual('1k'); + }); + it('formats value as bytes', () => { + expect( + formatStatValue({ + type: 'bytesPerSecond', + label: 'bytes stat', + value: 1, + } as Stat) + ).toEqual('1.0B/s'); + expect( + formatStatValue({ + type: 'bytesPerSecond', + label: 'bytes stat', + value: 1048576, + } as Stat) + ).toEqual('1.0MB/s'); + expect( + formatStatValue({ + type: 'bytesPerSecond', + label: 'bytes stat', + value: 1073741824, + } as Stat) + ).toEqual('1.0GB/s'); + }); + it('formats value as percent', () => { + const stat = { + type: 'percent', + label: 'percent stat', + value: 0.841, + } as Stat; + expect(formatStatValue(stat)).toEqual('84.1%'); + }); +}); diff --git a/x-pack/plugins/observability/public/utils/format_stat_value.ts b/x-pack/plugins/observability/public/utils/format_stat_value.ts new file mode 100644 index 0000000000000..c200d94d5699e --- /dev/null +++ b/x-pack/plugins/observability/public/utils/format_stat_value.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import numeral from '@elastic/numeral'; +import { Stat } from '../typings'; + +export function formatStatValue(stat: Stat) { + const { value, type } = stat; + switch (type) { + case 'bytesPerSecond': + return `${numeral(value).format('0.0b')}/s`; + case 'number': + return numeral(value).format('0a'); + case 'percent': + return numeral(value).format('0.0%'); + } +} diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js b/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js new file mode 100644 index 0000000000000..1608003641596 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js @@ -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 moment from 'moment'; +const d = moment.duration; + +const roundingRules = [ + [d(500, 'ms'), d(100, 'ms')], + [d(5, 'second'), d(1, 'second')], + [d(7.5, 'second'), d(5, 'second')], + [d(15, 'second'), d(10, 'second')], + [d(45, 'second'), d(30, 'second')], + [d(3, 'minute'), d(1, 'minute')], + [d(9, 'minute'), d(5, 'minute')], + [d(20, 'minute'), d(10, 'minute')], + [d(45, 'minute'), d(30, 'minute')], + [d(2, 'hour'), d(1, 'hour')], + [d(6, 'hour'), d(3, 'hour')], + [d(24, 'hour'), d(12, 'hour')], + [d(1, 'week'), d(1, 'd')], + [d(3, 'week'), d(1, 'week')], + [d(1, 'year'), d(1, 'month')], + [Infinity, d(1, 'year')], +]; + +const revRoundingRules = roundingRules.slice(0).reverse(); + +function find(rules, check, last) { + function pick(buckets, duration) { + const target = duration / buckets; + let lastResp = null; + + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + const resp = check(rule[0], rule[1], target); + + if (resp == null) { + if (!last) continue; + if (lastResp) return lastResp; + break; + } + + if (!last) return resp; + lastResp = resp; + } + + // fallback to just a number of milliseconds, ensure ms is >= 1 + const ms = Math.max(Math.floor(target), 1); + return moment.duration(ms, 'ms'); + } + + return (buckets, duration) => { + const interval = pick(buckets, duration); + if (interval) return moment.duration(interval._data); + }; +} + +export const calculateAuto = { + near: find( + revRoundingRules, + function near(bound, interval, target) { + if (bound > target) return interval; + }, + true + ), + + lessThan: find(revRoundingRules, function lessThan(_bound, interval, target) { + if (interval < target) return interval; + }), + + atLeast: find(revRoundingRules, function atLeast(_bound, interval, target) { + if (interval <= target) return interval; + }), +}; diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts new file mode 100644 index 0000000000000..39c4aedaa6013 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { getBucketSize } from './index'; +import moment from 'moment'; + +describe('getBuckets', () => { + describe("minInterval 'auto'", () => { + it('last 15 minutes', () => { + const start = moment().subtract(15, 'minutes').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 10, + intervalString: '10s', + }); + }); + it('last 1 hour', () => { + const start = moment().subtract(1, 'hour').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 30, + intervalString: '30s', + }); + }); + it('last 1 week', () => { + const start = moment().subtract(1, 'week').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 3600, + intervalString: '3600s', + }); + }); + it('last 30 days', () => { + const start = moment().subtract(30, 'days').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 43200, + intervalString: '43200s', + }); + }); + it('last 1 year', () => { + const start = moment().subtract(1, 'year').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 86400, + intervalString: '86400s', + }); + }); + }); + describe("minInterval '30s'", () => { + it('last 15 minutes', () => { + const start = moment().subtract(15, 'minutes').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({ + bucketSize: 30, + intervalString: '30s', + }); + }); + it('last 1 year', () => { + const start = moment().subtract(1, 'year').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({ + bucketSize: 86400, + intervalString: '86400s', + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts new file mode 100644 index 0000000000000..5673b890adf33 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +// @ts-ignore +import { calculateAuto } from './calculate_auto'; +import { unitToSeconds } from './unit_to_seconds'; + +export function getBucketSize({ + start, + end, + minInterval, +}: { + start: number; + end: number; + minInterval: string; +}) { + const duration = moment.duration(end - start, 'ms'); + const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1); + const intervalString = `${bucketSize}s`; + const matches = minInterval && minInterval.match(/^([\d]+)([shmdwMy]|ms)$/); + const minBucketSize = matches ? Number(matches[1]) * unitToSeconds(matches[2]) : 0; + + if (bucketSize < minBucketSize) { + return { + bucketSize: minBucketSize, + intervalString: minInterval, + }; + } + + return { bucketSize, intervalString }; +} diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts new file mode 100644 index 0000000000000..657726d988495 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.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 moment, { unitOfTime as UnitOfTIme } from 'moment'; + +function getDurationAsSeconds(value: number, unitOfTime: UnitOfTIme.Base) { + return moment.duration(value, unitOfTime).asSeconds(); +} + +const units = { + ms: getDurationAsSeconds(1, 'millisecond'), + s: getDurationAsSeconds(1, 'second'), + m: getDurationAsSeconds(1, 'minute'), + h: getDurationAsSeconds(1, 'hour'), + d: getDurationAsSeconds(1, 'day'), + w: getDurationAsSeconds(1, 'week'), + M: getDurationAsSeconds(1, 'month'), + y: getDurationAsSeconds(1, 'year'), +}; + +export function unitToSeconds(unit: string) { + return units[unit as keyof typeof units]; +} diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx new file mode 100644 index 0000000000000..2a290f2b24d6b --- /dev/null +++ b/x-pack/plugins/observability/public/utils/test_helper.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 React from 'react'; +import { render as testLibRender } from '@testing-library/react'; +import { AppMountContext } from 'kibana/public'; +import { PluginContext } from '../context/plugin_context'; +import { EuiThemeProvider } from '../typings'; + +export const core = ({ + http: { + basePath: { + prepend: jest.fn(), + }, + }, +} as unknown) as AppMountContext['core']; + +export const render = (component: React.ReactNode) => { + return testLibRender( + + {component} + + ); +}; diff --git a/x-pack/plugins/observability/public/utils/url.ts b/x-pack/plugins/observability/public/utils/url.ts new file mode 100644 index 0000000000000..962ab8233a8f5 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/url.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 { parse, stringify } from 'query-string'; +import { url } from '../../../../../src/plugins/kibana_utils/public'; + +export function toQuery(search?: string) { + return search ? parse(search.slice(1), { sort: false }) : {}; +} + +export function fromQuery(query: Record) { + const encodedQuery = url.encodeQuery(query, (value) => + encodeURIComponent(value).replace(/%3A/g, ':') + ); + + return stringify(encodedQuery, { sort: false, encode: false }); +} diff --git a/x-pack/plugins/observability/scripts/storybook.js b/x-pack/plugins/observability/scripts/storybook.js new file mode 100644 index 0000000000000..e9db98e2adf6b --- /dev/null +++ b/x-pack/plugins/observability/scripts/storybook.js @@ -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 { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'observability', + storyGlobs: [ + join(__dirname, '..', 'public', 'components', '**', '*.stories.tsx'), + join(__dirname, '..', 'public', 'pages', '**', '*.stories.tsx'), + ], +}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 961a046c846e4..9a9f445de0b13 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -28,9 +28,9 @@ export { runTaskFnFactory } from './server/execute_job'; export const getExportType = (): ExportTypeDefinition< JobParamsPanelCsv, - ImmediateCreateJobFn, + ImmediateCreateJobFn, JobParamsPanelCsv, - ImmediateExecuteFn + ImmediateExecuteFn > => ({ ...metadata, jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts index dafac04017607..da9810b03aff6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts @@ -20,15 +20,15 @@ import { } from '../../types'; import { createJobSearch } from './create_job_search'; -export type ImmediateCreateJobFn = ( - jobParams: JobParamsType, +export type ImmediateCreateJobFn = ( + jobParams: JobParamsPanelCsv, headers: KibanaRequest['headers'], context: RequestHandlerContext, req: KibanaRequest ) => Promise<{ type: string | null; title: string; - jobParams: JobParamsType; + jobParams: JobParamsPanelCsv; }>; interface VisData { @@ -37,9 +37,10 @@ interface VisData { panel: SearchPanel; } -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting, parentLogger) { +export const scheduleTaskFnFactory: ScheduleTaskFnFactory = function createJobFactoryFn( + reporting, + parentLogger +) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts index 26b7a24907f40..912ae0809cf92 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts @@ -7,39 +7,43 @@ import { i18n } from '@kbn/i18n'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory } from '../../../lib'; import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../../types'; import { CsvResultFromSearch } from '../../csv/types'; -import { FakeRequest, JobParamsPanelCsv, SearchPanel } from '../types'; +import { JobParamsPanelCsv, SearchPanel } from '../types'; import { createGenerateCsv } from './lib'; +/* + * The run function receives the full request which provides the un-encrypted + * headers, so encrypted headers are not part of these kind of job params + */ +type ImmediateJobParams = Omit, 'headers'>; + /* * ImmediateExecuteFn receives the job doc payload because the payload was * generated in the ScheduleFn */ -export type ImmediateExecuteFn = ( +export type ImmediateExecuteFn = ( jobId: null, - job: ScheduledTaskParams, + job: ImmediateJobParams, context: RequestHandlerContext, req: KibanaRequest ) => Promise; -export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); +export const runTaskFnFactory: RunTaskFnFactory = function executeJobFactoryFn( + reporting, + parentLogger +) { const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); const generateCsv = createGenerateCsv(reporting, parentLogger); - return async function runTask(jobId: string | null, job, context, req) { + return async function runTask(jobId: string | null, job, context, request) { // There will not be a jobID for "immediate" generation. // jobID is only for "queued" jobs // Use the jobID as a logging tag or "immediate" const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]); const { jobParams } = job; - const { isImmediate, panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; + const { panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; if (!panel) { i18n.translate( @@ -50,54 +54,13 @@ export const runTaskFnFactory: RunTaskFnFactory; - const serializedEncryptedHeaders = job.headers; - try { - if (typeof serializedEncryptedHeaders !== 'string') { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage', - { - defaultMessage: 'Job headers are missing', - } - ) - ); - } - decryptedHeaders = (await crypto.decrypt(serializedEncryptedHeaders)) as Record< - string, - unknown - >; - } catch (err) { - jobLogger.error(err); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage', - { - defaultMessage: - 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', - values: { encryptionKey: 'xpack.reporting.encryptionKey', err }, - } - ) - ); - } - - requestObject = { headers: decryptedHeaders }; - } - let content: string; let maxSizeReached = false; let size = 0; try { const generateResults: CsvResultFromSearch = await generateCsv( context, - requestObject, + request, visType as string, panel, jobParams diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts index 835b352953dfe..c182fe49a31f6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts @@ -23,10 +23,6 @@ export interface JobParamsPanelCsv { visType?: string; } -export interface ScheduledTaskParamsPanelCsv extends ScheduledTaskParams { - jobParams: JobParamsPanelCsv; -} - export interface SavedObjectServiceError { statusCode: number; error?: string; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts deleted file mode 100644 index b8326406743b7..0000000000000 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts +++ /dev/null @@ -1,85 +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 { schema } from '@kbn/config-schema'; -import { get } from 'lodash'; -import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; -import { ReportingCore } from '../'; -import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; -import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; - -/* - * This function registers API Endpoints for queuing Reporting jobs. The API inputs are: - * - saved object type and ID - * - time range and time zone - * - application state: - * - filters - * - query bar - * - local (transient) changes the user made to the saved object - */ -export function registerGenerateCsvFromSavedObject( - reporting: ReportingCore, - handleRoute: HandlerFunction, - handleRouteError: HandlerErrorFunction -) { - const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); - const { router } = setupDeps; - router.post( - { - path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, - validate: { - params: schema.object({ - savedObjectType: schema.string({ minLength: 2 }), - savedObjectId: schema.string({ minLength: 2 }), - }), - body: schema.object({ - state: schema.object({}), - timerange: schema.object({ - timezone: schema.string({ defaultValue: 'UTC' }), - min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), - max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), - }), - }), - }, - }, - userHandler(async (user, context, req, res) => { - /* - * 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle - * 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params - * 3. Ensure that details for a queued job were returned - */ - let result: QueuedJobPayload; - try { - const jobParams = getJobParamsFromRequest(req, { isImmediate: false }); - result = await handleRoute( - user, - CSV_FROM_SAVEDOBJECT_JOB_TYPE, - jobParams, - context, - req, - res - ); - } catch (err) { - return handleRouteError(res, err); - } - - if (get(result, 'source.job') == null) { - return res.badRequest({ - body: `The Export handler is expected to return a result with job info! ${result}`, - }); - } - - return res.ok({ - body: result, - headers: { - 'content-type': 'application/json', - }, - }); - }) - ); -} diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 7d93a36c85bc8..97441bba70984 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -10,7 +10,6 @@ import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/server/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/server/execute_job'; import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { ScheduledTaskParamsPanelCsv } from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../types'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; @@ -64,12 +63,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( const runTaskFn = runTaskFnFactory(reporting, logger); try { - const jobDocPayload: ScheduledTaskParamsPanelCsv = await scheduleTaskFn( - jobParams, - req.headers, - context, - req - ); + // FIXME: no scheduleTaskFn for immediate download + const jobDocPayload = await scheduleTaskFn(jobParams, req.headers, context, req); const { content_type: jobOutputContentType, content: jobOutputContent, @@ -91,11 +86,12 @@ export function registerGenerateCsvFromSavedObjectImmediate( return res.ok({ body: jobOutputContent || '', headers: { - 'content-type': jobOutputContentType, + 'content-type': jobOutputContentType ? jobOutputContentType : [], 'accept-ranges': 'none', }, }); } catch (err) { + logger.error(err); return handleError(res, err); } }) diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index 7de7c68122125..c73c443d2390b 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; +import { of } from 'rxjs'; +import sinon from 'sinon'; import { setupServer } from 'src/core/server/test_utils'; -import { registerJobGenerationRoutes } from './generation'; -import { createMockReportingCore } from '../test_helpers'; +import supertest from 'supertest'; import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { ExportTypeDefinition } from '../types'; -import { LevelLogger } from '../lib'; -import { of } from 'rxjs'; +import { createMockReportingCore } from '../test_helpers'; +import { createMockLevelLogger } from '../test_helpers/create_mock_levellogger'; +import { registerJobGenerationRoutes } from './generation'; type setupServerReturn = UnwrapPromise>; @@ -21,7 +21,8 @@ describe('POST /api/reporting/generate', () => { const reportingSymbol = Symbol('reporting'); let server: setupServerReturn['server']; let httpSetup: setupServerReturn['httpSetup']; - let exportTypesRegistry: ExportTypesRegistry; + let mockExportTypesRegistry: ExportTypesRegistry; + let callClusterStub: any; let core: ReportingCore; const config = { @@ -29,7 +30,7 @@ describe('POST /api/reporting/generate', () => { const key = args.join('.'); switch (key) { case 'queue.indexInterval': - return 10000; + return 'year'; case 'queue.timeout': return 10000; case 'index': @@ -42,56 +43,45 @@ describe('POST /api/reporting/generate', () => { }), kbnConfig: { get: jest.fn() }, }; - const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), - } as unknown) as jest.Mocked; + const mockLogger = createMockLevelLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({})); - const mockDeps = ({ + + callClusterStub = sinon.stub().resolves({}); + + const mockSetupDeps = ({ elasticsearch: { - legacy: { - client: { callAsInternalUser: jest.fn() }, - }, + legacy: { client: { callAsInternalUser: callClusterStub } }, }, security: { - license: { - isEnabled: () => true, - }, + license: { isEnabled: () => true }, authc: { - getCurrentUser: () => ({ - id: '123', - roles: ['superuser'], - username: 'Tom Riddle', - }), + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), }, }, router: httpSetup.createRouter(''), - licensing: { - license$: of({ - isActive: true, - isAvailable: true, - type: 'gold', - }), - }, + licensing: { license$: of({ isActive: true, isAvailable: true, type: 'gold' }) }, } as unknown) as any; - core = await createMockReportingCore(config, mockDeps); - exportTypesRegistry = new ExportTypesRegistry(); - exportTypesRegistry.register({ + + core = await createMockReportingCore(config, mockSetupDeps); + + mockExportTypesRegistry = new ExportTypesRegistry(); + mockExportTypesRegistry.register({ id: 'printablePdf', + name: 'not sure why this field exists', jobType: 'printable_pdf', jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['basic', 'gold'], - } as ExportTypeDefinition); - core.getExportTypesRegistry = () => exportTypesRegistry; + scheduleTaskFnFactory: () => () => ({ scheduleParamsTest: { test1: 'yes' } }), + runTaskFnFactory: () => () => ({ runParamsTest: { test2: 'yes' } }), + }); + core.getExportTypesRegistry = () => mockExportTypesRegistry; }); afterEach(async () => { - mockLogger.debug.mockReset(); - mockLogger.error.mockReset(); await server.stop(); }); @@ -147,14 +137,9 @@ describe('POST /api/reporting/generate', () => { ); }); - it('returns 400 if job handler throws an error', async () => { - const errorText = 'you found me'; - core.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ - toJSON: () => { - throw new Error(errorText); - }, - })); + it('returns 500 if job handler throws an error', async () => { + // throw an error from enqueueJob + core.getEnqueueJob = jest.fn().mockRejectedValue('Sorry, this tests says no'); registerJobGenerationRoutes(core, mockLogger); @@ -163,9 +148,27 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/printablePdf') .send({ jobParams: `abc` }) - .expect(400) + .expect(500); + }); + + it(`returns 200 if job handler doesn't error`, async () => { + callClusterStub.withArgs('index').resolves({ _id: 'foo', _index: 'foo-index' }); + + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `abc` }) + .expect(200) .then(({ body }) => { - expect(body.message).toMatchInlineSnapshot(`"${errorText}"`); + expect(body).toMatchObject({ + job: { + id: expect.any(String), + }, + path: expect.any(String), + }); }); }); }); diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index b4c81e698ce71..017e875931ae2 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -11,7 +11,6 @@ import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; -import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; import { HandlerFunction } from './types'; @@ -43,24 +42,32 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo return res.forbidden({ body: licenseResults.message }); } - const enqueueJob = await reporting.getEnqueueJob(); - const job = await enqueueJob(exportTypeId, jobParams, user, context, req); - - // return the queue's job information - const jobJson = job.toJSON(); - const downloadBaseUrl = getDownloadBaseUrl(reporting); - - return res.ok({ - headers: { - 'content-type': 'application/json', - }, - body: { - path: `${downloadBaseUrl}/${jobJson.id}`, - job: jobJson, - }, - }); + try { + const enqueueJob = await reporting.getEnqueueJob(); + const job = await enqueueJob(exportTypeId, jobParams, user, context, req); + + // return the queue's job information + const jobJson = job.toJSON(); + const downloadBaseUrl = getDownloadBaseUrl(reporting); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: { + path: `${downloadBaseUrl}/${jobJson.id}`, + job: jobJson, + }, + }); + } catch (err) { + logger.error(err); + throw err; + } }; + /* + * Error should already have been logged by the time we get here + */ function handleError(res: typeof kibanaResponseFactory, err: Error | Boom) { if (err instanceof Boom) { return res.customError({ @@ -87,12 +94,10 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo }); } - return res.badRequest({ - body: err.message, - }); + // unknown error, can't convert to 4xx + throw err; } registerGenerateFromJobParams(reporting, handler, handleError); - registerGenerateCsvFromSavedObject(reporting, handler, handleError); // FIXME: remove this https://github.com/elastic/kibana/issues/62986 registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger); } diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index a8492481e6b13..651f1c34fee6c 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -46,20 +46,20 @@ export function downloadJobResponseHandlerFactory(reporting: ReportingCore) { }); } - const response = getDocumentPayload(doc); + const payload = getDocumentPayload(doc); - if (!WHITELISTED_JOB_CONTENT_TYPES.includes(response.contentType)) { + if (!payload.contentType || !WHITELISTED_JOB_CONTENT_TYPES.includes(payload.contentType)) { return res.badRequest({ - body: `Unsupported content-type of ${response.contentType} specified by job output`, + body: `Unsupported content-type of ${payload.contentType} specified by job output`, }); } return res.custom({ - body: typeof response.content === 'string' ? Buffer.from(response.content) : response.content, - statusCode: response.statusCode, + body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, + statusCode: payload.statusCode, headers: { - ...response.headers, - 'content-type': response.contentType, + ...payload.headers, + 'content-type': payload.contentType || '', }, }); }; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 427a6362a7258..95b06aa39f07e 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -22,6 +22,7 @@ import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStartDeps } from '../types'; import { ReportingStore } from '../lib'; import { createMockLevelLogger } from './create_mock_levellogger'; +import { Report } from '../lib/store'; (initializeBrowserDriverFactory as jest.Mock< Promise @@ -47,7 +48,7 @@ const createMockPluginStart = ( const store = new ReportingStore(mockReportingCore, logger); return { browserDriverFactory: startMock.browserDriverFactory, - enqueueJob: startMock.enqueueJob, + enqueueJob: startMock.enqueueJob || jest.fn().mockResolvedValue(new Report({} as any)), esqueue: startMock.esqueue, savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 743510d45107e..d83d5ef3f6468 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -45,7 +45,9 @@ describe('', () => { let history: ScopedHistory; beforeEach(() => { - history = (scopedHistoryMock.create() as unknown) as ScopedHistory; + history = (scopedHistoryMock.create({ + createHref: jest.fn((location) => location.pathname!), + }) as unknown) as ScopedHistory; apiClientMock = rolesAPIClientMock.create(); apiClientMock.getRoles.mockResolvedValue([ { @@ -135,15 +137,19 @@ describe('', () => { }); expect(wrapper.find(PermissionDenied)).toHaveLength(0); - expect( - wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]') - ).toHaveLength(1); - expect( - wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-disabled-role"]') - ).toHaveLength(1); + + const editButton = wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]'); + expect(editButton).toHaveLength(1); + expect(editButton.prop('href')).toBe('/edit/test-role-1'); + + const cloneButton = wrapper.find( + 'EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]' + ); + expect(cloneButton).toHaveLength(1); + expect(cloneButton.prop('href')).toBe('/clone/test-role-1'); expect( - wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]') + wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-disabled-role"]') ).toHaveLength(1); expect( wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-disabled-role"]') diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 051c16f03d342..c2ea119100722 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -262,7 +262,7 @@ export class RolesGridPage extends Component { iconType={'copy'} {...reactRouterNavigate( this.props.history, - getRoleManagementHref('edit', role.name) + getRoleManagementHref('clone', role.name) )} /> ); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index a34a76361f799..7cd5692176ee3 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -36,7 +36,7 @@ export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*'; export enum SecurityPageName { - alerts = 'alerts', + detections = 'detections', overview = 'overview', hosts = 'hosts', network = 'network', @@ -46,12 +46,12 @@ export enum SecurityPageName { } export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`; -export const APP_ALERTS_PATH = `${APP_PATH}/alerts`; +export const APP_DETECTIONS_PATH = `${APP_PATH}/detections`; export const APP_HOSTS_PATH = `${APP_PATH}/hosts`; export const APP_NETWORK_PATH = `${APP_PATH}/network`; export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`; export const APP_CASES_PATH = `${APP_PATH}/cases`; -export const APP_MANAGEMENT_PATH = `${APP_PATH}/management`; +export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`; /** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ export const DEFAULT_INDEX_PATTERN = [ @@ -165,13 +165,6 @@ export const showAllOthersBucket: string[] = [ 'user.name', ]; -/** - * CreateTemplateTimelineBtn - * https://github.com/elastic/kibana/pull/66613 - * Remove the comment here to enable template timeline - */ -export const disableTemplate = false; - /* * This should be set to true after https://github.com/elastic/kibana/pull/67496 is merged */ diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 6720f3523d5c7..339e5554ccb12 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1036,8 +1036,8 @@ export class EndpointDocGenerator { config: { artifact_manifest: { value: { - manifest_version: 'v0', - schema_version: '1.0.0', + manifest_version: 'WzAsMF0=', + schema_version: 'v1', artifacts: {}, }, }, diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts index fdb2570314cd0..014673ebe6398 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts @@ -10,6 +10,7 @@ export const compressionAlgorithm = t.keyof({ none: null, zlib: null, }); +export type CompressionAlgorithm = t.TypeOf; export const encryptionAlgorithm = t.keyof({ none: null, @@ -20,7 +21,7 @@ export const identifier = t.string; export const manifestVersion = t.string; export const manifestSchemaVersion = t.keyof({ - '1.0.0': null, + v1: null, }); export type ManifestSchemaVersion = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 2cf5930a83bee..90d254b15e8b3 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -50,6 +50,16 @@ const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), }); +export enum DataProviderType { + default = 'default', + template = 'template', +} + +export const DataProviderTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(DataProviderType.default), + runtimeTypes.literal(DataProviderType.template), +]); + const SavedDataProviderRuntimeType = runtimeTypes.partial({ id: unionWithNullType(runtimeTypes.string), name: unionWithNullType(runtimeTypes.string), @@ -58,6 +68,7 @@ const SavedDataProviderRuntimeType = runtimeTypes.partial({ kqlQuery: unionWithNullType(runtimeTypes.string), queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), + type: unionWithNullType(DataProviderTypeLiteralRt), }); /* @@ -154,7 +165,7 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< >; /** - * Template timeline type + * Timeline template type */ export enum TemplateTimelineType { @@ -229,8 +240,8 @@ export interface SavedTimelineNote extends runtimeTypes.TypeOf { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); - loginAndWaitForPage(ALERTS_URL); + loginAndWaitForPage(DETECTIONS_URL); }); it('Closes and opens alerts', () => { @@ -162,7 +162,7 @@ describe.skip('Alerts', () => { context('Opening alerts', () => { beforeEach(() => { esArchiverLoad('closed_alerts'); - loginAndWaitForPage(ALERTS_URL); + loginAndWaitForPage(DETECTIONS_URL); }); it('Open one alert when more than one closed alerts are selected', () => { @@ -208,7 +208,7 @@ describe.skip('Alerts', () => { context('Marking alerts as in-progress', () => { beforeEach(() => { esArchiverLoad('alerts'); - loginAndWaitForPage(ALERTS_URL); + loginAndWaitForPage(DETECTIONS_URL); }); it('Mark one alert in progress when more than one open alerts are selected', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts index 5cad0b9c3260c..20cf624b3360d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts @@ -26,7 +26,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { ALERTS_URL } from '../urls/navigation'; +import { DETECTIONS_URL } from '../urls/navigation'; describe('Alerts detection rules', () => { before(() => { @@ -38,7 +38,7 @@ describe('Alerts detection rules', () => { }); it('Sorts by activated rules', () => { - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 684570450aa05..81832b3d9edea 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -62,7 +62,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { ALERTS_URL } from '../urls/navigation'; +import { DETECTIONS_URL } from '../urls/navigation'; // Flaky: https://github.com/elastic/kibana/issues/67814 describe.skip('Detection rules, custom', () => { @@ -75,7 +75,7 @@ describe.skip('Detection rules, custom', () => { }); it('Creates and activates a new custom rule', () => { - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); @@ -170,7 +170,7 @@ describe.skip('Detection rules, custom', () => { describe('Deletes custom rules', () => { beforeEach(() => { esArchiverLoad('custom_rules'); - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index fdab3016de8de..a7e6652613493 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -13,11 +13,12 @@ import { exportFirstRule } from '../tasks/alerts_detection_rules'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { ALERTS_URL } from '../urls/navigation'; +import { DETECTIONS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -describe('Export rules', () => { +// Flakky: https://github.com/elastic/kibana/issues/69849 +describe.skip('Export rules', () => { before(() => { esArchiverLoad('export_rule'); cy.server(); @@ -32,7 +33,7 @@ describe('Export rules', () => { }); it('Exports a custom rule', () => { - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 19957a53dd701..b6b30ef550eb1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -58,7 +58,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { ALERTS_URL } from '../urls/navigation'; +import { DETECTIONS_URL } from '../urls/navigation'; describe('Detection rules, machine learning', () => { before(() => { @@ -70,7 +70,7 @@ describe('Detection rules, machine learning', () => { }); it('Creates and activates a new ml rule', () => { - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts index d3cbb05d7fc17..986a7c7177a79 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts @@ -31,7 +31,7 @@ import { import { esArchiverLoadEmptyKibana, esArchiverUnloadEmptyKibana } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { ALERTS_URL } from '../urls/navigation'; +import { DETECTIONS_URL } from '../urls/navigation'; import { totalNumberOfPrebuiltRules } from '../objects/rule'; @@ -48,7 +48,7 @@ describe('Alerts rules, prebuilt rules', () => { const expectedNumberOfRules = totalNumberOfPrebuiltRules; const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); @@ -73,7 +73,7 @@ describe('Deleting prebuilt rules', () => { const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; esArchiverLoadEmptyKibana(); - loginAndWaitForPageWithoutDateRange(ALERTS_URL); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index 10dc4fdd44486..b37aabf4825fc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -15,12 +15,13 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPage } from '../tasks/login'; -import { ALERTS_URL } from '../urls/navigation'; +import { DETECTIONS_URL } from '../urls/navigation'; -describe('Alerts timeline', () => { +// Flakky: https://github.com/elastic/kibana/issues/71220 +describe.skip('Alerts timeline', () => { beforeEach(() => { esArchiverLoad('timeline_alerts'); - loginAndWaitForPage(ALERTS_URL); + loginAndWaitForPage(DETECTIONS_URL); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts index ea3a78c77152a..e4f0ec2c4828f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { - ALERTS, CASES, + DETECTIONS, HOSTS, MANAGEMENT, NETWORK, @@ -17,21 +17,21 @@ import { loginAndWaitForPage } from '../tasks/login'; import { navigateFromHeaderTo } from '../tasks/security_header'; import { - ALERTS_URL, + DETECTIONS_URL, CASES_URL, HOSTS_URL, KIBANA_HOME, - MANAGEMENT_URL, + ADMINISTRATION_URL, NETWORK_URL, OVERVIEW_URL, TIMELINES_URL, } from '../urls/navigation'; import { openKibanaNavigation, navigateFromKibanaCollapsibleTo } from '../tasks/kibana_navigation'; import { - ALERTS_PAGE, CASES_PAGE, + DETECTIONS_PAGE, HOSTS_PAGE, - MANAGEMENT_PAGE, + ADMINISTRATION_PAGE, NETWORK_PAGE, OVERVIEW_PAGE, TIMELINES_PAGE, @@ -47,9 +47,9 @@ describe('top-level navigation common to all pages in the Security app', () => { cy.url().should('include', OVERVIEW_URL); }); - it('navigates to the Alerts page', () => { - navigateFromHeaderTo(ALERTS); - cy.url().should('include', ALERTS_URL); + it('navigates to the Detections page', () => { + navigateFromHeaderTo(DETECTIONS); + cy.url().should('include', DETECTIONS_URL); }); it('navigates to the Hosts page', () => { @@ -72,9 +72,9 @@ describe('top-level navigation common to all pages in the Security app', () => { cy.url().should('include', CASES_URL); }); - it('navigates to the Management page', () => { + it('navigates to the Administration page', () => { navigateFromHeaderTo(MANAGEMENT); - cy.url().should('include', MANAGEMENT_URL); + cy.url().should('include', ADMINISTRATION_URL); }); }); @@ -90,9 +90,9 @@ describe('Kibana navigation to all pages in the Security app ', () => { cy.url().should('include', OVERVIEW_URL); }); - it('navigates to the Alerts page', () => { - navigateFromKibanaCollapsibleTo(ALERTS_PAGE); - cy.url().should('include', ALERTS_URL); + it('navigates to the Detections page', () => { + navigateFromKibanaCollapsibleTo(DETECTIONS_PAGE); + cy.url().should('include', DETECTIONS_URL); }); it('navigates to the Hosts page', () => { @@ -115,8 +115,8 @@ describe('Kibana navigation to all pages in the Security app ', () => { cy.url().should('include', CASES_URL); }); - it('navigates to the Management page', () => { - navigateFromKibanaCollapsibleTo(MANAGEMENT_PAGE); - cy.url().should('include', MANAGEMENT_URL); + it('navigates to the Administration page', () => { + navigateFromKibanaCollapsibleTo(ADMINISTRATION_PAGE); + cy.url().should('include', ADMINISTRATION_URL); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index 911fd7e0f3483..205a49fc771cf 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -9,9 +9,9 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('URL compatibility', () => { - it('Redirects to Alerts from old Detections URL', () => { + it('Redirects to Detection alerts from old Detections URL', () => { loginAndWaitForPage(DETECTIONS); - cy.url().should('include', '/security/alerts'); + cy.url().should('include', '/security/detections'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts index 2f7956ce370bc..68352c6e584cc 100644 --- a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts +++ b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ALERTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Alerts"]'; +export const DETECTIONS_PAGE = + '[data-test-subj="collapsibleNavGroup-security"] [title="Detections"]'; export const CASES_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Cases"]'; @@ -12,8 +13,8 @@ export const HOSTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [titl export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]'; -export const MANAGEMENT_PAGE = - '[data-test-subj="collapsibleNavGroup-security"] [title="Management"]'; +export const ADMINISTRATION_PAGE = + '[data-test-subj="collapsibleNavGroup-security"] [title="Administration"]'; export const NETWORK_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Network"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts index 17d8aed1c2d21..20fcae60415ae 100644 --- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ALERTS = '[data-test-subj="navigation-alerts"]'; +export const DETECTIONS = '[data-test-subj="navigation-detections"]'; export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index c673cf34b6dae..14282b84b5ffc 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -9,7 +9,7 @@ export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const DRAGGABLE_HEADER = - '[data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; + '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; export const HEADERS_GROUP = '[data-test-subj="headers-group"]'; @@ -21,7 +21,8 @@ export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; -export const REMOVE_COLUMN = '[data-test-subj="remove-column"]'; +export const REMOVE_COLUMN = + '[data-test-subj="events-viewer-panel"] [data-test-subj="remove-column"]'; export const RESET_FIELDS = '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 761fd2c1e6a0b..37ce9094dc594 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -27,8 +27,6 @@ import { import { drag, drop } from '../tasks/common'; -export const hostExistsQuery = 'host.name: *'; - export const addDescriptionToTimeline = (description: string) => { cy.get(TIMELINE_DESCRIPTION).type(`${description}{enter}`); cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).click().invoke('text').should('not.equal', 'Updating'); @@ -79,7 +77,6 @@ export const openTimelineSettings = () => { }; export const populateTimeline = () => { - executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') .then((strCount) => { diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index 9da9abf388e4d..b53b06db5beda 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ALERTS_URL = 'app/security/alerts'; +export const DETECTIONS_URL = 'app/security/detections'; export const CASES_URL = '/app/security/cases'; export const DETECTIONS = '/app/siem#/detections'; export const HOSTS_URL = '/app/security/hosts/allHosts'; @@ -16,7 +16,7 @@ export const HOSTS_PAGE_TAB_URLS = { uncommonProcesses: '/app/security/hosts/uncommonProcesses', }; export const KIBANA_HOME = '/app/home#/'; -export const MANAGEMENT_URL = '/app/security/management'; +export const ADMINISTRATION_URL = '/app/security/administration'; export const NETWORK_URL = '/app/security/network'; export const OVERVIEW_URL = '/app/security/overview'; export const TIMELINES_URL = '/app/security/timelines'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts deleted file mode 100644 index ad4f5cf8b4aa8..0000000000000 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts +++ /dev/null @@ -1,274 +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 { cloneDeep } from 'lodash/fp'; - -import { mockEcsData } from '../../../common/mock/mock_ecs'; -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; - -import { - getStringArray, - replaceTemplateFieldFromQuery, - replaceTemplateFieldFromMatchFilters, - reformatDataProviderWithNewValue, -} from './helpers'; - -describe('helpers', () => { - let mockEcsDataClone = cloneDeep(mockEcsData); - beforeEach(() => { - mockEcsDataClone = cloneDeep(mockEcsData); - }); - describe('getStringOrStringArray', () => { - test('it should correctly return a string array', () => { - const value = getStringArray('x', { - x: 'The nickname of the developer we all :heart:', - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with a single element', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:'], - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with two elements of strings', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], - }); - expect(value).toEqual([ - 'The nickname of the developer we all :heart:', - 'We are all made of stars', - ]); - }); - - test('it should correctly return a string array with deep elements', () => { - const value = getStringArray('x.y.z', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual(['zed']); - }); - - test('it should correctly return a string array with a non-existent value', () => { - const value = getStringArray('non.existent', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual([]); - }); - - test('it should trace an error if the value is not a string', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: 5 }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - 5, - 'when trying to access field:', - 'a', - 'from data object of:', - { a: 5 } - ); - }); - - test('it should trace an error if the value is an array of mixed values', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - ['hi', 5], - 'when trying to access field:', - 'a', - 'from data object of:', - { a: ['hi', 5] } - ); - }); - }); - - describe('replaceTemplateFieldFromQuery', () => { - test('given an empty query string this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('given a query string with spaces this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('it should replace a query with a template value such as apache from a mock template', () => { - const replacement = replaceTemplateFieldFromQuery( - 'host.name: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('host.name: apache'); - }); - - test('it should replace a template field with an ECS value that is not an array', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); - expect(replacement).toEqual('host.name: *'); - }); - - test('it should NOT replace a query with a template value that is not part of the template fields array', () => { - const replacement = replaceTemplateFieldFromQuery( - 'user.id: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('user.id: placeholdertext'); - }); - }); - - describe('replaceTemplateFieldFromMatchFilters', () => { - test('given an empty query filter this will return an empty filter', () => { - const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); - expect(replacement).toEqual([]); - }); - - test('given a query filter this will return that filter with the placeholder replaced', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Braden' }, - }, - query: { match_phrase: { 'host.name': 'Braden' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'apache' }, - }, - query: { match_phrase: { 'host.name': 'apache' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - - test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - }); - - describe('reformatDataProviderWithNewValue', () => { - test('it should replace a query with a template value such as apache from a mock data provider', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'user.id'; - mockDataProvider.id = 'my-id'; - mockDataProvider.name = 'Rebecca'; - mockDataProvider.queryMatch.value = 'Rebecca'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'my-id', - name: 'Rebecca', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.id', - value: 'Rebecca', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx index 88e9d4179a971..543a4634ceecc 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx @@ -9,7 +9,7 @@ import { SecurityPageName } from '../types'; import { SiemNavTab } from '../../common/components/navigation/types'; import { APP_OVERVIEW_PATH, - APP_ALERTS_PATH, + APP_DETECTIONS_PATH, APP_HOSTS_PATH, APP_NETWORK_PATH, APP_TIMELINES_PATH, @@ -25,12 +25,12 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'overview', }, - [SecurityPageName.alerts]: { - id: SecurityPageName.alerts, - name: i18n.Alerts, - href: APP_ALERTS_PATH, + [SecurityPageName.detections]: { + id: SecurityPageName.detections, + name: i18n.DETECTION_ENGINE, + href: APP_DETECTIONS_PATH, disabled: false, - urlKey: 'alerts', + urlKey: 'detections', }, [SecurityPageName.hosts]: { id: SecurityPageName.hosts, @@ -63,7 +63,7 @@ export const navTabs: SiemNavTab = { }, [SecurityPageName.management]: { id: SecurityPageName.management, - name: i18n.MANAGEMENT, + name: i18n.ADMINISTRATION, href: APP_MANAGEMENT_PATH, disabled: false, urlKey: SecurityPageName.management, 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 03e48282cb754..8f03945df437c 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -17,6 +17,7 @@ import { UseUrlState } from '../../common/components/url_state'; import { useWithSource } from '../../common/containers/source'; 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` height: 100%; @@ -55,9 +56,17 @@ export const HomePage: React.FC = ({ children }) => { }), [windowHeight] ); + const { signalIndexExists, signalIndexName } = useSignalIndex(); + + const indexToAdd = useMemo(() => { + if (signalIndexExists && signalIndexName != null) { + return [signalIndexName]; + } + return null; + }, [signalIndexExists, signalIndexName]); const [showTimeline] = useShowTimeline(); - const { browserFields, indexPattern, indicesExist } = useWithSource(); + const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); return ( diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/home/translations.ts index f5a08e6395f1f..bee1dfe333851 100644 --- a/x-pack/plugins/security_solution/public/app/home/translations.ts +++ b/x-pack/plugins/security_solution/public/app/home/translations.ts @@ -25,7 +25,7 @@ export const DETECTION_ENGINE = i18n.translate( } ); -export const Alerts = i18n.translate('xpack.securitySolution.navigation.alerts', { +export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { defaultMessage: 'Alerts', }); @@ -37,6 +37,6 @@ export const CASE = i18n.translate('xpack.securitySolution.navigation.case', { defaultMessage: 'Cases', }); -export const MANAGEMENT = i18n.translate('xpack.securitySolution.navigation.management', { - defaultMessage: 'Management', +export const ADMINISTRATION = i18n.translate('xpack.securitySolution.navigation.administration', { + defaultMessage: 'Administration', }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 2de957039efe6..bf134a02dd822 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -26,7 +26,7 @@ import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers import { useGetCases, UpdateCase } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useDeleteCases } from '../../containers/use_delete_cases'; -import { EuiBasicTableOnChange } from '../../../alerts/pages/detection_engine/rules/types'; +import { EuiBasicTableOnChange } from '../../../detections/pages/detection_engine/rules/types'; import { Panel } from '../../../common/components/panel'; import { UtilityBar, diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts index 8403050a13114..b1ab509417fe5 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts @@ -30,7 +30,7 @@ export const ALERTS_TABLE_TITLE = i18n.translate( export const ALERTS_GRAPH_TITLE = i18n.translate( 'xpack.securitySolution.alertsView.alertsGraphTitle', { - defaultMessage: 'External alert count', + defaultMessage: 'External alert trend', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts index 4fb4e5d30ca7a..ba328eff62e51 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts @@ -182,6 +182,11 @@ export const addProviderToTimeline = ({ } }; +const linkFields: Record = { + 'signal.rule.name': 'signal.rule.id', + 'event.module': 'rule.reference', +}; + export const addFieldToTimelineColumns = ({ upsertColumn = timelineActions.upsertColumn, browserFields, @@ -202,6 +207,7 @@ export const addFieldToTimelineColumns = ({ description: isString(column.description) ? column.description : undefined, example: isString(column.example) ? column.example : undefined, id: fieldId, + linkField: linkFields[fieldId] ?? undefined, type: column.type, aggregatable: column.aggregatable, width: DEFAULT_COLUMN_MIN_WIDTH, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index ec56751b4cbd2..2a079ce015f0d 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -14,13 +14,13 @@ import { wait } from '../../lib/helpers'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { defaultHeaders } from './default_headers'; -import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; -jest.mock('../../../alerts/containers/detection_engine/rules/fetch_index_patterns'); +jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ { browserFields: mockBrowserFields, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index edab0e3a98456..a5f4dc0c5ed6f 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -14,12 +14,12 @@ import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; -import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; -jest.mock('../../../alerts/containers/detection_engine/rules/fetch_index_patterns'); +jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ { browserFields: mockBrowserFields, 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 1645db371802c..02b3571421f67 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 @@ -21,7 +21,7 @@ import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/eve import { Filter } from '../../../../../../../src/plugins/data/public'; import { useUiSetting } from '../../lib/kibana'; import { EventsViewer } from './events_viewer'; -import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { InspectButtonContainer } from '../inspect'; export interface OwnProps { 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 5221e170574b3..be89aa8e33718 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 @@ -34,7 +34,7 @@ import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toas import { ExceptionBuilder } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; -import { useSignalIndex } from '../../../../alerts/containers/detection_engine/alerts/use_signal_index'; +import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -44,7 +44,7 @@ import { entryHasListType, entryHasNonEcsType, } from '../helpers'; -import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules'; +import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; export interface AddExceptionOnClick { ruleName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx new file mode 100644 index 0000000000000..791782b0f0152 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx @@ -0,0 +1,438 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { EntryItemComponent } from './entry_item'; +import { + isOperator, + isNotOperator, + isOneOfOperator, + isNotOneOfOperator, + isInListOperator, + isNotInListOperator, + existsOperator, + doesNotExistOperator, +} from '../../autocomplete/operators'; +import { + fields, + getField, +} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { getFoundListSchemaMock } from '../../../../../../lists/common/schemas/response/found_list_schema.mock'; +import { getEmptyValue } from '../../empty_value'; + +// mock out lists hook +const mockStart = jest.fn(); +const mockResult = getFoundListSchemaMock(); +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../lists_plugin_deps', () => { + const originalModule = jest.requireActual('../../../../lists_plugin_deps'); + + return { + ...originalModule, + useFindLists: () => ({ + loading: false, + start: mockStart.mockReturnValue(mockResult), + result: mockResult, + error: undefined, + }), + }; +}); + +describe('EntryItemComponent', () => { + test('it renders fields disabled if "isLoading" is "true"', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryField"] input').props().disabled + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"] input').props().disabled + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"] input').props().disabled + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldFormRow"]')).toHaveLength(0); + }); + + test('it renders field labels if "showLabel" is "true"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldFormRow"]')).not.toEqual(0); + }); + + test('it renders field values correctly when operator is "isOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + '1234' + ); + }); + + test('it renders field values correctly when operator is "isNotOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is not' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + '1234' + ); + }); + + test('it renders field values correctly when operator is "isOneOfOperator"', () => { + const wrapper = mount( + + ); + + 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( + '1234' + ); + }); + + test('it renders field values correctly when operator is "isNotOneOfOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is not one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + '1234' + ); + }); + + test('it renders field values correctly when operator is "isInListOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is in list' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldList"]').text()).toEqual( + 'some name' + ); + }); + + test('it renders field values correctly when operator is "isNotInListOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is not in list' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldList"]').text()).toEqual( + 'some name' + ); + }); + + test('it renders field values correctly when operator is "existsOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'exists' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').text()).toEqual( + getEmptyValue() + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"] input').props().disabled + ).toBeTruthy(); + }); + + test('it renders field values correctly when operator is "doesNotExistOperator"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'does not exist' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').text()).toEqual( + getEmptyValue() + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"] input').props().disabled + ).toBeTruthy(); + }); + + test('it invokes "onChange" when new field is selected and resets operator and value fields', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + ); + + ((wrapper.find(EuiComboBox).at(0).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'machine.os' }]); + + expect(mockOnChange).toHaveBeenCalledWith( + { field: 'machine.os', operator: 'included', type: 'match', value: undefined }, + 0 + ); + }); + + test('it invokes "onChange" when new operator is selected and resets value field', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + ); + + ((wrapper.find(EuiComboBox).at(1).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'is not' }]); + + expect(mockOnChange).toHaveBeenCalledWith( + { field: 'ip', operator: 'excluded', type: 'match', value: '' }, + 0 + ); + }); + + test('it invokes "onChange" when new value field is entered for match operator', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + ); + + ((wrapper.find(EuiComboBox).at(2).props() as unknown) as { + onCreateOption: (a: string) => void; + }).onCreateOption('126.45.211.34'); + + expect(mockOnChange).toHaveBeenCalledWith( + { field: 'ip', operator: 'excluded', type: 'match', value: '126.45.211.34' }, + 0 + ); + }); + + test('it invokes "onChange" when new value field is entered for match_any operator', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + ); + + ((wrapper.find(EuiComboBox).at(2).props() as unknown) as { + onCreateOption: (a: string) => void; + }).onCreateOption('126.45.211.34'); + + expect(mockOnChange).toHaveBeenCalledWith( + { field: 'ip', operator: 'included', type: 'match_any', value: ['126.45.211.34'] }, + 0 + ); + }); + + test('it invokes "onChange" when new value field is entered for list operator', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + ); + + ((wrapper.find(EuiComboBox).at(2).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'some name' }]); + + expect(mockOnChange).toHaveBeenCalledWith( + { + field: 'ip', + operator: 'excluded', + type: 'list', + list: { id: 'some-list-id', type: 'ip' }, + }, + 0 + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 39a1e1bdbad5a..0f5000c8c0abe 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -67,13 +67,13 @@ export const EntryItemComponent: React.FC = ({ { field: entry.field != null ? entry.field.name : undefined, type: OperatorTypeEnum.MATCH, - operator: isOperator.operator, + operator: entry.operator.operator, value: newField, }, entryIndex ); }, - [onChange, entryIndex, entry.field] + [onChange, entryIndex, entry.field, entry.operator.operator] ); const handleFieldMatchAnyValueChange = useCallback( @@ -82,13 +82,13 @@ export const EntryItemComponent: React.FC = ({ { field: entry.field != null ? entry.field.name : undefined, type: OperatorTypeEnum.MATCH_ANY, - operator: isOperator.operator, + operator: entry.operator.operator, value: newField, }, entryIndex ); }, - [onChange, entryIndex, entry.field] + [onChange, entryIndex, entry.field, entry.operator.operator] ); const handleFieldListValueChange = useCallback( @@ -97,13 +97,13 @@ export const EntryItemComponent: React.FC = ({ { field: entry.field != null ? entry.field.name : undefined, type: OperatorTypeEnum.LIST, - operator: isOperator.operator, + operator: entry.operator.operator, list: { id: newField.id, type: newField.type }, }, entryIndex ); }, - [onChange, entryIndex, entry.field] + [onChange, entryIndex, entry.field, entry.operator.operator] ); const renderFieldInput = (isFirst: boolean): JSX.Element => { @@ -114,9 +114,9 @@ export const EntryItemComponent: React.FC = ({ selectedField={entry.field} isLoading={isLoading} isClearable={false} - isDisabled={indexPattern == null} + isDisabled={isLoading} onChange={handleFieldChange} - data-test-subj="filterFieldSuggestionList" + data-test-subj="exceptionBuilderEntryField" /> ); @@ -137,11 +137,11 @@ export const EntryItemComponent: React.FC = ({ placeholder={i18n.EXCEPTION_OPERATOR_PLACEHOLDER} selectedField={entry.field} operator={entry.operator} - isDisabled={false} + isDisabled={isLoading} isLoading={false} isClearable={false} onChange={handleOperatorChange} - data-test-subj="filterFieldSuggestionList" + data-test-subj="exceptionBuilderEntryOperator" /> ); @@ -165,12 +165,12 @@ export const EntryItemComponent: React.FC = ({ placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER} selectedField={entry.field} selectedValue={value} - isDisabled={false} + isDisabled={isLoading} isLoading={isLoading} isClearable={false} indexPattern={indexPattern} onChange={handleFieldMatchValueChange} - data-test-subj="filterFieldSuggestionList" + data-test-subj="exceptionBuilderEntryFieldMatch" /> ); case OperatorTypeEnum.MATCH_ANY: @@ -180,12 +180,12 @@ export const EntryItemComponent: React.FC = ({ placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER} selectedField={entry.field} selectedValue={values} - isDisabled={false} + isDisabled={isLoading} isLoading={isLoading} isClearable={false} indexPattern={indexPattern} onChange={handleFieldMatchAnyValueChange} - data-test-subj="filterFieldSuggestionList" + data-test-subj="exceptionBuilderEntryFieldMatchAny" /> ); case OperatorTypeEnum.LIST: @@ -195,17 +195,18 @@ export const EntryItemComponent: React.FC = ({ selectedField={entry.field} placeholder={i18n.EXCEPTION_FIELD_LISTS_PLACEHOLDER} selectedValue={id} - isLoading={false} - isDisabled={false} + isLoading={isLoading} + isDisabled={isLoading} isClearable={false} onChange={handleFieldListValueChange} + data-test-subj="exceptionBuilderEntryFieldList" /> ); case OperatorTypeEnum.EXISTS: return ( ); default: diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx index 3afdf43ec7dfa..5e53ce3ba6578 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx @@ -29,6 +29,7 @@ interface ExceptionListItemProps { isLoading: boolean; indexPattern: IIndexPattern; andLogicIncluded: boolean; + onCheckAndLogic: (item: ExceptionsBuilderExceptionItem[]) => void; onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onExceptionItemChange: (item: ExceptionsBuilderExceptionItem, index: number) => void; } @@ -41,6 +42,7 @@ export const ExceptionListItemComponent = React.memo( indexPattern, isLoading, andLogicIncluded, + onCheckAndLogic, onDeleteExceptionItem, onExceptionItemChange, }) => { @@ -70,11 +72,12 @@ export const ExceptionListItemComponent = React.memo( onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); }; - const entries = useMemo( - (): FormattedBuilderEntry[] => - indexPattern != null ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) : [], - [indexPattern, exceptionItem.entries] - ); + const entries = useMemo((): FormattedBuilderEntry[] => { + onCheckAndLogic([exceptionItem]); + return indexPattern != null + ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) + : []; + }, [indexPattern, exceptionItem, onCheckAndLogic]); const andBadge = useMemo((): JSX.Element => { const badge = ; 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 c6376c34c768f..d3ed1dfc944fd 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 @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { ExceptionListItemComponent } from './exception_item'; -import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { ExceptionListItemSchema, NamespaceType, @@ -77,15 +77,21 @@ export const ExceptionBuilder = ({ indexPatternConfig ?? [] ); + const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => { + setAndLogicIncluded((includesAnd: boolean): boolean => { + if (includesAnd) { + return true; + } else { + return items.filter(({ entries }) => entries.length > 1).length > 0; + } + }); + }; + // Bubble up changes to parent useEffect(() => { onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); }, [onChange, exceptionsToDelete, exceptions]); - const checkAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => { - setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0); - }; - const handleDeleteExceptionItem = ( item: ExceptionsBuilderExceptionItem, itemIndex: number @@ -100,7 +106,7 @@ export const ExceptionBuilder = ({ ...existingExceptions.slice(0, itemIndex), ...existingExceptions.slice(itemIndex + 1), ]; - checkAndLogic(updatedExceptions); + handleCheckAndLogic(updatedExceptions); return updatedExceptions; }); @@ -118,7 +124,7 @@ export const ExceptionBuilder = ({ ...exceptions.slice(index + 1), ]; - checkAndLogic(updatedExceptions); + handleCheckAndLogic(updatedExceptions); setExceptions(updatedExceptions); }; @@ -214,6 +220,7 @@ export const ExceptionBuilder = ({ isLoading={indexPatternLoading} exceptionItemIndex={index} andLogicIncluded={andLogicIncluded} + onCheckAndLogic={handleCheckAndLogic} onDeleteExceptionItem={handleDeleteExceptionItem} onExceptionItemChange={handleExceptionItemChange} /> 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 4bec5778cd775..aa36b65e04b69 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 @@ -21,8 +21,8 @@ import { EuiText, } from '@elastic/eui'; import { alertsIndexPattern } from '../../../../../common/endpoint/constants'; -import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules'; -import { useSignalIndex } from '../../../../alerts/containers/detection_engine/alerts/use_signal_index'; +import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; +import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index b167807a6edaa..018ca1d29c369 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -7,7 +7,7 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; import { KibanaServices } from '../../../common/lib/kibana'; -import * as alertsApi from '../../../alerts/containers/detection_engine/alerts/api'; +import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api'; import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 2d793c89e48f1..267a9afd9cf6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -14,8 +14,8 @@ import { CreateExceptionListItemSchema, UpdateExceptionListItemSchema, } from '../../../lists_plugin_deps'; -import { updateAlertStatus } from '../../../alerts/containers/detection_engine/alerts/api'; -import { getUpdateAlertsQuery } from '../../../alerts/components/alerts_table/actions'; +import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api'; +import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions'; import { formatExceptionItemForUpdate } from './helpers'; /** diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 1a031abc56f35..afc3568fd6c65 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -6,10 +6,10 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; -import * as rulesApi from '../../../alerts/containers/detection_engine/rules/api'; +import * as rulesApi from '../../../detections/containers/detection_engine/rules/api'; import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; -import { savedRuleMock } from '../../../alerts/containers/detection_engine/rules/mock'; +import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { ExceptionListType } from '../../../lists_plugin_deps'; import { ListArray } from '../../../../common/detection_engine/schemas/types'; @@ -21,7 +21,7 @@ import { } from './use_fetch_or_create_rule_exception_list'; const mockKibanaHttpService = createKibanaCoreStartMock().http; -jest.mock('../../../alerts/containers/detection_engine/rules/api'); +jest.mock('../../../detections/containers/detection_engine/rules/api'); describe('useFetchOrCreateRuleExceptionList', () => { let fetchRuleById: jest.SpyInstance>; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 5000a79287fc0..245ce192b3cfa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -11,9 +11,12 @@ import { ExceptionListSchema, CreateExceptionListSchema, } from '../../../../../lists/common/schemas'; -import { Rule } from '../../../alerts/containers/detection_engine/rules/types'; +import { Rule } from '../../../detections/containers/detection_engine/rules/types'; import { List, ListArray } from '../../../../common/detection_engine/schemas/types'; -import { fetchRuleById, patchRule } from '../../../alerts/containers/detection_engine/rules/api'; +import { + fetchRuleById, + patchRule, +} from '../../../detections/containers/detection_engine/rules/api'; import { fetchExceptionListById, addExceptionList } from '../../../lists_plugin_deps'; export type ReturnUseFetchOrCreateRuleExceptionList = [boolean, ExceptionListSchema | null]; diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx index 2e8d5f77afc83..33e26cd4db035 100644 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import { isFunction } from 'lodash/fp'; import * as i18n from './translations'; -import { ExportDocumentsProps } from '../../../alerts/containers/detection_engine/rules'; +import { ExportDocumentsProps } from '../../../detections/containers/detection_engine/rules'; import { useStateToaster, errorToToaster } from '../toasters'; const InvisibleAnchor = styled.a` 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 17fdf2163b58e..ba4f782499802 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 @@ -19,7 +19,7 @@ import * as i18n from './translations'; import { useWithSource } from '../../containers/source'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; -import { APP_ID, ADD_DATA_PATH, APP_ALERTS_PATH } from '../../../../common/constants'; +import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; import { LinkAnchor } from '../links'; const Wrapper = styled.header` @@ -60,7 +60,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - + @@ -70,7 +70,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine display="condensed" navTabs={ hideDetectionEngine - ? pickBy((_, key) => key !== SecurityPageName.alerts, navTabs) + ? pickBy((_, key) => key !== SecurityPageName.detections, navTabs) : navTabs } /> @@ -86,7 +86,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - {indicesExist && window.location.pathname.includes(APP_ALERTS_PATH) && ( + {indicesExist && window.location.pathname.includes(APP_DETECTIONS_PATH) && ( 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 a42628cecff8e..d5d670b4c03ff 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 @@ -24,7 +24,7 @@ import React, { useCallback, useState } from 'react'; import { ImportDataResponse, ImportDataProps, -} from '../../../alerts/containers/detection_engine/rules'; +} from '../../../detections/containers/detection_engine/rules'; import { displayErrorToast, displaySuccessToast, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 5c1c68b802726..dc5324adbac7d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -13,7 +13,7 @@ import { StartServices } from '../../../../types'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/ip_details'; import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; -import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../alerts/pages/detection_engine/rules/utils'; +import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; import { SecurityPageName } from '../../../../app/types'; import { @@ -59,7 +59,7 @@ const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => spyState != null && spyState.pageName === SecurityPageName.case; const isAlertsRoutes = (spyState: RouteSpyState) => - spyState != null && spyState.pageName === SecurityPageName.alerts; + spyState != null && spyState.pageName === SecurityPageName.detections; export const getBreadcrumbsForRoute = ( object: RouteSpyState & TabNavigationProps, @@ -103,7 +103,7 @@ export const getBreadcrumbsForRoute = ( ]; } if (isAlertsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'alerts', isDetailPage: false }; + const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; if (spyState.tabName != null) { urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index 10f8b11b4d9c5..229e2d2402298 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -92,12 +92,12 @@ describe('SIEM Navigation', () => { { detailName: undefined, navTabs: { - alerts: { + detections: { disabled: false, - href: '/app/security/alerts', - id: 'alerts', - name: 'Alerts', - urlKey: 'alerts', + href: '/app/security/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', }, case: { disabled: false, @@ -108,9 +108,9 @@ describe('SIEM Navigation', () => { }, management: { disabled: false, - href: '/app/security/management', + href: '/app/security/administration', id: 'management', - name: 'Management', + name: 'Administration', urlKey: 'management', }, hosts: { @@ -197,12 +197,12 @@ describe('SIEM Navigation', () => { filters: [], flowTarget: undefined, navTabs: { - alerts: { + detections: { disabled: false, - href: '/app/security/alerts', - id: 'alerts', - name: 'Alerts', - urlKey: 'alerts', + href: '/app/security/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', }, case: { disabled: false, @@ -220,9 +220,9 @@ describe('SIEM Navigation', () => { }, management: { disabled: false, - href: '/app/security/management', + href: '/app/security/administration', id: 'management', - name: 'Management', + name: 'Administration', urlKey: 'management', }, network: { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 80302be18355c..0489ebba738c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -45,7 +45,7 @@ export type SiemNavTabKey = | SecurityPageName.overview | SecurityPageName.hosts | SecurityPageName.network - | SecurityPageName.alerts + | SecurityPageName.detections | SecurityPageName.timelines | SecurityPageName.case | SecurityPageName.management; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index da6ec784af6d4..c8232b0c3b3cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -364,7 +364,7 @@ describe('StatefulTopN', () => { field={field} indexPattern={mockIndexPattern} indexToAdd={null} - timelineId={TimelineId.alertsPage} + timelineId={TimelineId.detectionsPage} toggleTopN={jest.fn()} onFilterAdded={jest.fn()} value={value} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index 0b2f1f1e35cc7..807f1839973fa 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -132,7 +132,8 @@ const StatefulTopNComponent: React.FC = ({ } data-test-subj="top-n" defaultView={ - timelineId === TimelineId.alertsPage || timelineId === TimelineId.alertsRulesDetailsPage + timelineId === TimelineId.detectionsPage || + timelineId === TimelineId.detectionsRulesDetailsPage ? 'alert' : options[0].value } diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 71faec88e85a0..1faff2594ce80 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -8,7 +8,7 @@ export enum CONSTANTS { appQuery = 'query', caseDetails = 'case.details', casePage = 'case.page', - alertsPage = 'alerts.page', + detectionsPage = 'detections.page', filters = 'filters', hostsDetails = 'hosts.details', hostsPage = 'hosts.page', @@ -25,7 +25,7 @@ export enum CONSTANTS { export type UrlStateType = | 'case' - | 'alerts' + | 'detections' | 'host' | 'network' | 'overview' diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 7f4267bc5e2b3..6febf95aae01d 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -90,8 +90,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'host'; } else if (pageName === SecurityPageName.network) { return 'network'; - } else if (pageName === SecurityPageName.alerts) { - return 'alerts'; + } else if (pageName === SecurityPageName.detections) { + return 'detections'; } else if (pageName === SecurityPageName.timelines) { return 'timeline'; } else if (pageName === SecurityPageName.case) { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 20374affbdf89..eeeaacc25a15e 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -193,7 +193,7 @@ describe('UrlStateContainer', () => { wrapper.update(); await wait(); - if (CONSTANTS.alertsPage === page) { + if (CONSTANTS.detectionsPage === page) { expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ from: 11223344556677, fromStr: 'now-1d/d', diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index 8ca43cb576d32..8881a82e5cd1c 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -32,7 +32,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ ]; export const URL_STATE_KEYS: Record = { - alerts: [ + detections: [ CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, @@ -80,7 +80,7 @@ export const URL_STATE_KEYS: Record = { export type LocationTypes = | CONSTANTS.caseDetails | CONSTANTS.casePage - | CONSTANTS.alertsPage + | CONSTANTS.detectionsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage | CONSTANTS.networkDetails diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 221df436402dd..c97be1fdfb99b 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -203,7 +203,7 @@ export const useUrlStateHooks = ({ } }); } else if (pathName !== prevProps.pathName) { - handleInitialize(type, pageName === SecurityPageName.alerts); + handleInitialize(type, pageName === SecurityPageName.detections); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isInitializing, history, pathName, pageName, prevProps, urlState]); diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 4eb66acdfad65..5248136437d7d 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -10,7 +10,7 @@ import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types'; import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query'; -import { CreateTimelineProps } from '../../alerts/components/alerts_table/types'; +import { CreateTimelineProps } from '../../detections/components/alerts_table/types'; import { TimelineModel } from '../../timelines/store/timeline/model'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; export interface MockedProvidedQuery { diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index a9c6660ba9c68..14c38c5d6dab6 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -7,7 +7,7 @@ import { useState, useEffect } from 'react'; import { useRouteSpy } from '../route/use_route_spy'; -const hideTimelineForRoutes = [`/cases/configure`, '/management']; +const hideTimelineForRoutes = [`/cases/configure`, '/administration']; export const useShowTimeline = () => { const [{ pageName, pathName }] = useRouteSpy(); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/config.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/config.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 2923446b8322d..59d97480418b7 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -73,7 +73,7 @@ describe('AlertsHistogramPanel', () => { preventDefault: jest.fn(), }); - expect(mockNavigateToApp).toBeCalledWith('securitySolution:alerts', { path: '' }); + expect(mockNavigateToApp).toBeCalledWith('securitySolution:detections', { path: '' }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 533a9d51a9bcd..ba12499b8f20e 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -123,7 +123,7 @@ export const AlertsHistogramPanel = memo( ); const kibana = useKibana(); const { navigateToApp } = kibana.services.application; - const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.detections); const totalAlerts = useMemo( () => @@ -146,7 +146,7 @@ export const AlertsHistogramPanel = memo( const goToDetectionEngine = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, { + navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getDetectionEngineUrl(urlSearch), }); }, diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts index 6eaa0ba3fc4ec..e7c08914964a4 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts @@ -92,7 +92,7 @@ export const TOP = (fieldName: string) => export const HISTOGRAM_HEADER = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle', { - defaultMessage: 'Alert count', + defaultMessage: 'Trend', } ); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/types.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_info/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_info/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_info/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_info/query.dsl.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_info/query.dsl.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_info/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_info/types.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_info/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index bd62b79a3c54e..2fa7cfeedcd15 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -215,8 +215,8 @@ describe('alert actions', () => { columnId: '@timestamp', sortDirection: 'desc', }, - status: TimelineStatus.active, - title: 'Test rule - Duplicate', + status: TimelineStatus.draft, + title: '', timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index ba392e9904cc4..24f292cf9135b 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -10,7 +10,14 @@ import moment from 'moment'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; -import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../graphql/types'; +import { + TimelineNonEcsData, + GetOneTimeline, + TimelineResult, + Ecs, + TimelineStatus, + TimelineType, +} from '../../../graphql/types'; import { oneTimelineQuery } from '../../../timelines/containers/one/index.gql_query'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { @@ -122,20 +129,31 @@ export const sendAlertToTimelineAction = async ({ if (!isEmpty(resultingTimeline)) { const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); openAlertInBasicTimeline = false; - const { timeline } = formatTimelineResultToModel(timelineTemplate, true); + const { timeline } = formatTimelineResultToModel( + timelineTemplate, + true, + timelineTemplate.timelineType ?? TimelineType.default + ); const query = replaceTemplateFieldFromQuery( timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', - ecsData + ecsData, + timeline.timelineType ); const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); const dataProviders = replaceTemplateFieldFromDataProviders( timeline.dataProviders ?? [], - ecsData + ecsData, + timeline.timelineType ); + createTimeline({ from, timeline: { ...timeline, + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + status: TimelineStatus.draft, dataProviders, eventType: 'all', filters, diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts new file mode 100644 index 0000000000000..4decddd6b8886 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts @@ -0,0 +1,482 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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/fp'; + +import { TimelineType } from '../../../../common/types/timeline'; +import { mockEcsData } from '../../../common/mock/mock_ecs'; +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { + DataProvider, + DataProviderType, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; + +import { + getStringArray, + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + reformatDataProviderWithNewValue, +} from './helpers'; + +describe('helpers', () => { + let mockEcsDataClone = cloneDeep(mockEcsData); + beforeEach(() => { + mockEcsDataClone = cloneDeep(mockEcsData); + }); + describe('getStringOrStringArray', () => { + test('it should correctly return a string array', () => { + const value = getStringArray('x', { + x: 'The nickname of the developer we all :heart:', + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with a single element', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:'], + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with two elements of strings', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + }); + expect(value).toEqual([ + 'The nickname of the developer we all :heart:', + 'We are all made of stars', + ]); + }); + + test('it should correctly return a string array with deep elements', () => { + const value = getStringArray('x.y.z', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual(['zed']); + }); + + test('it should correctly return a string array with a non-existent value', () => { + const value = getStringArray('non.existent', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual([]); + }); + + test('it should trace an error if the value is not a string', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: 5 }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + 5, + 'when trying to access field:', + 'a', + 'from data object of:', + { a: 5 } + ); + }); + + test('it should trace an error if the value is an array of mixed values', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + ['hi', 5], + 'when trying to access field:', + 'a', + 'from data object of:', + { a: ['hi', 5] } + ); + }); + }); + + describe('replaceTemplateFieldFromQuery', () => { + describe('timelineType default', () => { + test('given an empty query string this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery( + '', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual(''); + }); + + test('given a query string with spaces this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery( + ' ', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual(''); + }); + + test('it should replace a query with a template value such as apache from a mock template', () => { + const replacement = replaceTemplateFieldFromQuery( + 'host.name: placeholdertext', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual('host.name: apache'); + }); + + test('it should replace a template field with an ECS value that is not an array', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const replacement = replaceTemplateFieldFromQuery( + 'host.name: *', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual('host.name: *'); + }); + + test('it should NOT replace a query with a template value that is not part of the template fields array', () => { + const replacement = replaceTemplateFieldFromQuery( + 'user.id: placeholdertext', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual('user.id: placeholdertext'); + }); + }); + + describe('timelineType template', () => { + test('given an empty query string this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery( + '', + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual(''); + }); + + test('given a query string with spaces this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery( + ' ', + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual(''); + }); + + test('it should NOT replace a query with a template value such as apache from a mock template', () => { + const replacement = replaceTemplateFieldFromQuery( + 'host.name: placeholdertext', + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual('host.name: placeholdertext'); + }); + + test('it should NOT replace a template field with an ECS value that is not an array', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const replacement = replaceTemplateFieldFromQuery( + 'host.name: *', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual('host.name: *'); + }); + + test('it should NOT replace a query with a template value that is not part of the template fields array', () => { + const replacement = replaceTemplateFieldFromQuery( + 'user.id: placeholdertext', + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual('user.id: placeholdertext'); + }); + }); + }); + + describe('replaceTemplateFieldFromMatchFilters', () => { + test('given an empty query filter this will return an empty filter', () => { + const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); + expect(replacement).toEqual([]); + }); + + test('given a query filter this will return that filter with the placeholder replaced', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Braden' }, + }, + query: { match_phrase: { 'host.name': 'Braden' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'apache' }, + }, + query: { match_phrase: { 'host.name': 'apache' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + + test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + }); + + describe('reformatDataProviderWithNewValue', () => { + describe('timelineType default', () => { + test('it should replace a query with a template value such as apache from a mock data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: TimelineType.default, + }); + }); + + test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: TimelineType.default, + }); + }); + + test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'user.id'; + mockDataProvider.id = 'my-id'; + mockDataProvider.name = 'Rebecca'; + mockDataProvider.queryMatch.value = 'Rebecca'; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.default + ); + expect(replacement).toEqual({ + id: 'my-id', + name: 'Rebecca', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.id', + value: 'Rebecca', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: TimelineType.default, + }); + }); + }); + + describe('timelineType template', () => { + test('it should replace a query with a template value such as apache from a mock data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = '{host.name}'; + mockDataProvider.type = DataProviderType.template; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: DataProviderType.default, + }); + }); + + test('it should NOT replace a query for default data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = '{host.name}'; + mockDataProvider.type = DataProviderType.default; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual({ + id: 'Braden', + name: 'Braden', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: '{host.name}', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: DataProviderType.default, + }); + }); + + test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = '{host.name}'; + mockDataProvider.type = DataProviderType.template; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: DataProviderType.default, + }); + }); + + test('it should replace a query with a template value that is not part of a template such as user.id', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'user.id'; + mockDataProvider.id = 'my-id'; + mockDataProvider.name = 'Rebecca'; + mockDataProvider.queryMatch.value = 'Rebecca'; + mockDataProvider.type = DataProviderType.default; + const replacement = reformatDataProviderWithNewValue( + mockDataProvider, + mockEcsDataClone[0], + TimelineType.template + ); + expect(replacement).toEqual({ + id: 'my-id', + name: 'Rebecca', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.id', + value: 'Rebecca', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + type: DataProviderType.default, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts similarity index 69% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts index 11a03b0426891..5025d782e2aa2 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts @@ -8,9 +8,10 @@ import { get, isEmpty } from 'lodash/fp'; import { Filter, esKuery, KueryNode } from '../../../../../../../src/plugins/data/public'; import { DataProvider, + DataProviderType, DataProvidersAnd, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Ecs } from '../../../graphql/types'; +import { Ecs, TimelineType } from '../../../graphql/types'; interface FindValueToChangeInQuery { field: string; @@ -101,20 +102,28 @@ export const findValueToChangeInQuery = ( ); }; -export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { - if (query.trim() !== '') { - const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); - return valueToChange.reduce((newQuery, vtc) => { - const newValue = getStringArray(vtc.field, ecsData); - if (newValue.length) { - return newQuery.replace(vtc.valueToChange, newValue[0]); - } else { - return newQuery; - } - }, query); - } else { - return ''; +export const replaceTemplateFieldFromQuery = ( + query: string, + ecsData: Ecs, + timelineType: TimelineType = TimelineType.default +): string => { + if (timelineType === TimelineType.default) { + if (query.trim() !== '') { + const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); + return valueToChange.reduce((newQuery, vtc) => { + const newValue = getStringArray(vtc.field, ecsData); + if (newValue.length) { + return newQuery.replace(vtc.valueToChange, newValue[0]); + } else { + return newQuery; + } + }, query); + } else { + return ''; + } } + + return query.trim(); }; export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => @@ -135,30 +144,64 @@ export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: export const reformatDataProviderWithNewValue = ( dataProvider: T, - ecsData: Ecs + ecsData: Ecs, + timelineType: TimelineType = TimelineType.default ): T => { - if (templateFields.includes(dataProvider.queryMatch.field)) { - const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); - if (newValue.length) { + // Support for legacy "template-like" timeline behavior that is using hardcoded list of templateFields + if (timelineType === TimelineType.default) { + if (templateFields.includes(dataProvider.queryMatch.field)) { + const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + if (newValue.length) { + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); + dataProvider.name = newValue[0]; + dataProvider.queryMatch.value = newValue[0]; + dataProvider.queryMatch.displayField = undefined; + dataProvider.queryMatch.displayValue = undefined; + } + } + dataProvider.type = DataProviderType.default; + return dataProvider; + } + + if (timelineType === TimelineType.template) { + if ( + dataProvider.type === DataProviderType.template && + dataProvider.queryMatch.operator === ':' + ) { + const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + + if (!newValue.length) { + dataProvider.enabled = false; + } + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); dataProvider.name = newValue[0]; dataProvider.queryMatch.value = newValue[0]; dataProvider.queryMatch.displayField = undefined; dataProvider.queryMatch.displayValue = undefined; + dataProvider.type = DataProviderType.default; + + return dataProvider; } + + dataProvider.type = dataProvider.type ?? DataProviderType.default; + + return dataProvider; } + return dataProvider; }; export const replaceTemplateFieldFromDataProviders = ( dataProviders: DataProvider[], - ecsData: Ecs + ecsData: Ecs, + timelineType: TimelineType = TimelineType.default ): DataProvider[] => dataProviders.map((dataProvider) => { - const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); + const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData, timelineType); if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { newDataProvider.and = newDataProvider.and.map((andDataProvider) => - reformatDataProviderWithNewValue(andDataProvider, ecsData) + reformatDataProviderWithNewValue(andDataProvider, ecsData, timelineType) ); } return newDataProvider; diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 6cbf69f409dc4..81aebe95930ac 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -13,7 +13,7 @@ import { Dispatch } from 'redux'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; -import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HeaderSection } from '../../../common/components/header_section'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; @@ -375,7 +375,7 @@ export const AlertsTableComponent: React.FC = ({ loadingText: i18n.LOADING_ALERTS, selectAll: canUserCRUD ? selectAll : false, timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], - title: i18n.ALERTS_TABLE_TITLE, + title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -408,7 +408,7 @@ export const AlertsTableComponent: React.FC = ({ if (loading || isEmpty(signalsIndex)) { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts similarity index 96% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index c9cf4e484910c..0f55469bbfda2 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -10,13 +10,6 @@ export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine defaultMessage: 'Detection engine', }); -export const ALERTS_TABLE_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.tableTitle', - { - defaultMessage: 'Alert list', - } -); - export const ALERTS_DOCUMENT_TYPE = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.documentTypeTitle', { diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/types.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts b/x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts b/x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/actions_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/actions_description.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/actions_description.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/actions_description.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/assets/list_tree_icon.svg b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/assets/list_tree_icon.svg similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/assets/list_tree_icon.svg rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/assets/list_tree_icon.svg diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/throttle_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/throttle_description.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/types.ts b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/types.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/detections/components/rules/next_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index d82b930210ecd..f93f380469622 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -36,7 +36,7 @@ const PrePackagedRulesPromptComponent: React.FC = ( const handlePreBuiltCreation = useCallback(() => { createPrePackagedRules(); }, [createPrePackagedRules]); - const { formatUrl } = useFormatUrl(SecurityPageName.alerts); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); const goToCreateRule = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/query_bar/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index a5d0382ef8c8c..cf7a485c59cb3 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -16,7 +16,7 @@ import styled from 'styled-components'; import { noop } from 'lodash/fp'; import { useHistory } from 'react-router-dom'; -import { Rule, exportRules } from '../../../../alerts/containers/detection_engine/rules'; +import { Rule, exportRules } from '../../../containers/detection_engine/rules'; import * as i18n from './translations'; import * as i18nActions from '../../../pages/detection_engine/rules/translations'; import { displaySuccessToast, useStateToaster } from '../../../../common/components/toasters'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts similarity index 85% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts index 88fca3d95604e..e99894afeb63c 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleStatusType } from '../../../../alerts/containers/detection_engine/rules'; +import { RuleStatusType } from '../../../containers/detection_engine/rules'; export const getStatusColor = (status: RuleStatusType | string | null) => status == null diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx index 53be48bc98850..0ddf4d06fb0fc 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx @@ -15,10 +15,7 @@ import { import React, { memo, useCallback, useEffect, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import { - useRuleStatus, - RuleInfoStatus, -} from '../../../../alerts/containers/detection_engine/rules'; +import { useRuleStatus, RuleInfoStatus } from '../../../containers/detection_engine/rules'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { getStatusColor } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index c85676ce51052..73d66bf024a62 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -16,7 +16,7 @@ import styled from 'styled-components'; import React, { useCallback, useState, useEffect } from 'react'; import * as i18n from '../../../pages/detection_engine/rules/translations'; -import { enableRules } from '../../../../alerts/containers/detection_engine/rules'; +import { enableRules } from '../../../containers/detection_engine/rules'; import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; import { Action } from '../../../pages/detection_engine/rules/all/reducer'; import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index b56e1794eef63..864f953bff1e1 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -13,7 +13,7 @@ import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules'; +import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { useMlCapabilities } from '../../../../common/components/ml_popover/hooks/use_ml_capabilities'; import { useUiSetting$ } from '../../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/types.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/types.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 061b8b0f8c36e..7005bfb25f4a6 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -88,7 +88,7 @@ const StepRuleActionsComponent: FC = ({ // TO DO need to make sure that logic is still valid const kibanaAbsoluteUrl = useMemo(() => { - const url = application.getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + const url = application.getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { absolute: true, }); if (url != null && url.includes('app/security/alerts')) { diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.test.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/translations.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/translations.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.test.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/mock.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/types.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/index.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts similarity index 90% rename from x-pack/plugins/security_solution/public/alerts/index.ts rename to x-pack/plugins/security_solution/public/detections/index.ts index a2e377a732936..d043127a3098b 100644 --- a/x-pack/plugins/security_solution/public/alerts/index.ts +++ b/x-pack/plugins/security_solution/public/detections/index.ts @@ -11,11 +11,11 @@ import { AlertsRoutes } from './routes'; import { SecuritySubPlugin } from '../app/types'; const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [ - TimelineId.alertsRulesDetailsPage, - TimelineId.alertsPage, + TimelineId.detectionsRulesDetailsPage, + TimelineId.detectionsPage, ]; -export class Alerts { +export class Detections { public setup() {} public start(storage: Storage): SecuritySubPlugin { diff --git a/x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts rename to x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts diff --git a/x-pack/plugins/security_solution/public/alerts/mitre/types.ts b/x-pack/plugins/security_solution/public/detections/mitre/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/mitre/types.ts rename to x-pack/plugins/security_solution/public/detections/mitre/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index b39d51e2de95f..11f738320db6e 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -56,7 +56,7 @@ export const DetectionEnginePageComponent: React.FC = ({ } = useUserInfo(); const history = useHistory(); const [lastAlerts] = useAlertInfo({}); - const { formatUrl } = useFormatUrl(SecurityPageName.alerts); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -146,7 +146,7 @@ export const DetectionEnginePageComponent: React.FC = ({ /> = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts similarity index 96% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index f1416bfbc41b5..2b86abf4255c6 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -5,9 +5,9 @@ */ import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; -import { Rule, RuleError } from '../../../../../../alerts/containers/detection_engine/rules'; +import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; -import { FieldValueQueryBar } from '../../../../../../alerts/components/rules/query_bar'; +import { FieldValueQueryBar } from '../../../../../components/rules/query_bar'; export const mockQueryBar: FieldValueQueryBar = { query: { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx index 5169ff009d63c..bad99039d0398 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx @@ -12,7 +12,7 @@ import { duplicateRules, enableRules, Rule, -} from '../../../../../alerts/containers/detection_engine/rules'; +} from '../../../../containers/detection_engine/rules'; import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx index 2c94588ce128a..71cfbbf552d84 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx @@ -15,7 +15,7 @@ import { exportRulesAction, } from './actions'; import { ActionToaster, displayWarningToast } from '../../../../../common/components/toasters'; -import { Rule } from '../../../../../alerts/containers/detection_engine/rules'; +import { Rule } from '../../../../containers/detection_engine/rules'; import * as detectionI18n from '../../translations'; interface GetBatchItems { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 030f510b7aa37..ea36a0cb0b48d 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -19,7 +19,7 @@ import * as H from 'history'; import React, { Dispatch } from 'react'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; -import { Rule, RuleStatus } from '../../../../../alerts/containers/detection_engine/rules'; +import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx index 7350cec0115fb..062d7967bf301 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx @@ -7,7 +7,7 @@ import { bucketRulesResponse, showRulesTable } from './helpers'; import { mockRule, mockRuleError } from './__mocks__/mock'; import uuid from 'uuid'; -import { Rule, RuleError } from '../../../../../alerts/containers/detection_engine/rules'; +import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; describe('AllRulesTable Helpers', () => { const mockRule1: Readonly = mockRule(uuid.v4()); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts similarity index 94% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts index 632d03cebef71..0ebeb84d57468 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts @@ -7,7 +7,7 @@ import { BulkRuleResponse, RuleResponseBuckets, -} from '../../../../../alerts/containers/detection_engine/rules'; +} from '../../../../containers/detection_engine/rules'; /** * Separates rules/errors from bulk rules API response (create/update/delete) diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index ae5c129befa5d..45e609e38202a 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -80,7 +80,7 @@ jest.mock('./reducer', () => { }; }); -jest.mock('../../../../../alerts/containers/detection_engine/rules', () => { +jest.mock('../../../../containers/detection_engine/rules', () => { return { useRules: jest.fn().mockReturnValue([ false, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 65f7bb63c74e4..85dce907084e8 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -24,7 +24,7 @@ import { Rule, PaginationOptions, exportRules, -} from '../../../../../alerts/containers/detection_engine/rules'; +} from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../../common/components/header_section'; import { UtilityBar, @@ -142,7 +142,7 @@ export const AllRules = React.memo( const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); - const { formatUrl } = useFormatUrl(SecurityPageName.alerts); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/reducer.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts index 3fe17fcaeeb9c..ff9b41bed06f5 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts @@ -9,7 +9,7 @@ import { FilterOptions, PaginationOptions, Rule, -} from '../../../../../alerts/containers/detection_engine/rules'; +} from '../../../../containers/detection_engine/rules'; type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null; export interface State { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index c65271c3cc014..0f201fcbaa441 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -16,8 +16,8 @@ import { import { isEqual } from 'lodash/fp'; import * as i18n from '../../translations'; -import { FilterOptions } from '../../../../../../alerts/containers/detection_engine/rules'; -import { useTags } from '../../../../../../alerts/containers/detection_engine/rules/use_tags'; +import { FilterOptions } from '../../../../../containers/detection_engine/rules'; +import { useTags } from '../../../../../containers/detection_engine/rules/use_tags'; import { TagsFilterPopover } from './tags_filter_popover'; interface RulesTableFiltersProps { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index bbfbbaae058d4..f402303c4c621 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NewRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { NewRule } from '../../../../containers/detection_engine/rules'; import { DefineStepRuleJson, ScheduleStepRuleJson, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index b7cf94bb4f319..8331346b19ac9 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -12,7 +12,7 @@ import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/const import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; -import { NewRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { NewRule } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 4be408039d6f6..6475b6f6b6b54 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; -import { usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { usePersistRule } from '../../../../containers/detection_engine/rules'; import { getRulesUrl, @@ -293,7 +293,7 @@ const CreateRulePageComponent: React.FC = () => { backOptions={{ href: getRulesUrl(), text: i18n.BACK_TO_RULES, - pageId: SecurityPageName.alerts, + pageId: SecurityPageName.detections, }} border isLoading={isLoading || loading} @@ -438,7 +438,7 @@ const CreateRulePageComponent: React.FC = () => { - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/translations.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx index fc16bcd96f766..c44f1bf780944 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx @@ -9,8 +9,8 @@ import { shallow } from 'enzyme'; import { TestProviders } from '../../../../../common/mock'; import { FailureHistory } from './failure_history'; -import { useRuleStatus } from '../../../../../alerts/containers/detection_engine/rules'; -jest.mock('../../../../../alerts/containers/detection_engine/rules'); +import { useRuleStatus } from '../../../../containers/detection_engine/rules'; +jest.mock('../../../../containers/detection_engine/rules'); describe('FailureHistory', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx index 1030aaa30d752..610b7e32cec5f 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx @@ -15,10 +15,7 @@ import { } from '@elastic/eui'; import React, { memo } from 'react'; -import { - useRuleStatus, - RuleInfoStatus, -} from '../../../../../alerts/containers/detection_engine/rules'; +import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../../common/components/header_section'; import * as i18n from './translations'; import { FormattedDate } from '../../../../../common/components/formatted_date'; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index bb6ef92a059cb..6ab08d94fa781 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -33,7 +33,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; -import { useRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { useRule } from '../../../../containers/detection_engine/rules'; import { useWithSource } from '../../../../../common/containers/source'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; @@ -130,7 +130,7 @@ export const RuleDetailsPageComponent: FC = ({ const [lastAlerts] = useAlertInfo({ ruleId }); const mlCapabilities = useMlCapabilities(); const history = useHistory(); - const { formatUrl } = useFormatUrl(SecurityPageName.alerts); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = @@ -302,7 +302,7 @@ export const RuleDetailsPageComponent: FC = ({ backOptions={{ href: getRulesUrl(), text: i18n.BACK_TO_RULES, - pageId: SecurityPageName.alerts, + pageId: SecurityPageName.detections, }} border subtitle={subTitle} @@ -424,7 +424,7 @@ export const RuleDetailsPageComponent: FC = ({ {ruleId != null && ( = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index ba7444d8e8a52..777f7766993d0 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -19,7 +19,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { useRule, usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { getRuleDetailsUrl, @@ -380,7 +380,7 @@ const EditRulePageComponent: FC = () => { backOptions={{ href: getRuleDetailsUrl(ruleId ?? ''), text: `${i18n.BACK_TO} ${rule?.name ?? ''}`, - pageId: SecurityPageName.alerts, + pageId: SecurityPageName.detections, }} isLoading={isLoading} title={i18n.PAGE_TITLE} @@ -446,7 +446,7 @@ const EditRulePageComponent: FC = () => { - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index b467f3334508d..f8969f06c8ef6 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -18,7 +18,7 @@ import { } from './helpers'; import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../../alerts/containers/detection_engine/rules'; +import { Rule } from '../../../containers/detection_engine/rules'; import { AboutStepRule, AboutStepRuleDetails, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 2a792f7d35eaa..bf49ed5be90fb 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -14,7 +14,7 @@ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engin import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../../alerts/containers/detection_engine/rules'; +import { Rule } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../../shared_imports'; import { AboutStepRule, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx similarity index 86% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 7e6cd48ddc003..f0ad670ddb665 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { RulesPage } from './index'; import { useUserInfo } from '../../../components/user_info'; -import { usePrePackagedRules } from '../../../../alerts/containers/detection_engine/rules'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -24,7 +24,7 @@ jest.mock('react-router-dom', () => { jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); -jest.mock('../../../../alerts/containers/detection_engine/rules'); +jest.mock('../../../containers/detection_engine/rules'); describe('RulesPage', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 7684f710952e6..9cbc0e2aabfbe 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -8,10 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { - usePrePackagedRules, - importRules, -} from '../../../../alerts/containers/detection_engine/rules'; +import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; import { getDetectionEngineUrl, getCreateRuleUrl, @@ -66,7 +63,7 @@ const RulesPageComponent: React.FC = () => { rulesNotInstalled, rulesNotUpdated ); - const { formatUrl } = useFormatUrl(SecurityPageName.alerts); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); const handleRefreshRules = useCallback(async () => { if (refreshRulesData.current != null) { @@ -126,8 +123,8 @@ const RulesPageComponent: React.FC = () => { @@ -203,7 +200,7 @@ const RulesPageComponent: React.FC = () => { /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts similarity index 99% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index eee2aa9ff40cc..050c281d09350 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; -export const BACK_TO_ALERTS = i18n.translate( +export const BACK_TO_DETECTIONS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.backOptionsHeader', { - defaultMessage: 'Back to alerts', + defaultMessage: 'Back to detections', } ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts similarity index 90% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts index 91de1467a8310..32f96b519acc5 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts @@ -23,6 +23,6 @@ describe('getBreadcrumbs', () => { [], getUrlForAppMock ) - ).toEqual([{ href: 'securitySolution:alerts', text: 'Alerts' }]); + ).toEqual([{ href: 'securitySolution:detections', text: 'Detection alerts' }]); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts similarity index 87% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 203a93acd849c..75d1df9406d25 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -28,7 +28,7 @@ const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetU if (tabPath === 'alerts') { return { text: i18nDetections.ALERT, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getDetectionEngineTabUrl(tabPath, !isEmpty(search[0]) ? search[0] : ''), }), }; @@ -37,7 +37,7 @@ const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetU if (tabPath === 'rules') { return { text: i18nRules.PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getRulesUrl(!isEmpty(search[0]) ? search[0] : ''), }), }; @@ -58,7 +58,7 @@ export const getBreadcrumbs = ( let breadcrumb = [ { text: i18nDetections.PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: !isEmpty(search[0]) ? search[0] : '', }), }, @@ -75,7 +75,7 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: params.state.ruleName, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getRuleDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), }), }, @@ -87,7 +87,7 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: i18nRules.ADD_PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getCreateRuleUrl(!isEmpty(search[0]) ? search[0] : ''), }), }, @@ -99,7 +99,7 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: i18nRules.EDIT_PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getEditRuleUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), }), }, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts similarity index 98% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts index 8e91d9848d1ed..bfe5dfc012530 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const PAGE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.detectionsPageTitle', { - defaultMessage: 'Alerts', + defaultMessage: 'Detection alerts', } ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/types.ts diff --git a/x-pack/plugins/security_solution/public/alerts/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/alerts/routes.tsx rename to x-pack/plugins/security_solution/public/detections/routes.tsx diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 86ee84f2e8bf4..2b8b07cb6a24b 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -10015,6 +10015,14 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "type", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "DataProviderType", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "and", "description": "", @@ -10088,6 +10096,29 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "DataProviderType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "default", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "template", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "DateRangePickerResult", @@ -11253,6 +11284,12 @@ } }, "defaultValue": null + }, + { + "name": "type", + "description": "", + "type": { "kind": "ENUM", "name": "DataProviderType", "ofType": null }, + "defaultValue": null } ], "interfaces": null, diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index bf5725c2ddea5..2c8f2e63356e6 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -185,6 +185,8 @@ export interface DataProviderInput { queryMatch?: Maybe; and?: Maybe; + + type?: Maybe; } export interface QueryMatchInput { @@ -342,6 +344,11 @@ export enum TlsFields { _id = '_id', } +export enum DataProviderType { + default = 'default', + template = 'template', +} + export enum TimelineStatus { active = 'active', draft = 'draft', @@ -2030,6 +2037,8 @@ export interface DataProviderResult { queryMatch?: Maybe; + type?: Maybe; + and?: Maybe; } @@ -5523,6 +5532,8 @@ export namespace GetOneTimeline { kqlQuery: Maybe; + type: Maybe; + queryMatch: Maybe; and: Maybe; diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts index 0dd66d06b78be..53fe185ef9a65 100644 --- a/x-pack/plugins/security_solution/public/helpers.ts +++ b/x-pack/plugins/security_solution/public/helpers.ts @@ -60,7 +60,7 @@ export const manageOldSiemRoutes = async (coreStart: CoreStart) => { }); break; case 'detections': - application.navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, { + application.navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { replace: true, path, }); diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 0fad1273c7279..4bc586bdee8a9 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -10,7 +10,7 @@ import { SecurityPageName } from '../../app/types'; // --[ ROUTING ]--------------------------------------------------------------------------- export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`; export const MANAGEMENT_ROUTING_ROOT_PATH = ''; -export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; +export const MANAGEMENT_ROUTING_HOSTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.hosts})`; export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`; @@ -21,5 +21,5 @@ export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace = export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList'; /** Namespace within the Management state where policy details state is maintained */ export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails'; -/** Namespace within the Management state where endpoints state is maintained */ -export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints'; +/** Namespace within the Management state where hosts state is maintained */ +export const MANAGEMENT_STORE_HOSTS_NAMESPACE = 'hosts'; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 92eb7717318d3..5add6b753a7a9 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -10,7 +10,7 @@ import { generatePath } from 'react-router-dom'; import querystring from 'querystring'; import { - MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_HOSTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, } from './constants'; @@ -32,11 +32,11 @@ const querystringStringify: ( ) => string = querystring.stringify; /** Make `selected_host` required */ -type EndpointDetailsUrlProps = Omit & +type HostDetailsUrlProps = Omit & Required>; -export const getEndpointListPath = ( - props: { name: 'default' | 'endpointList' } & HostIndexUIQueryParams, +export const getHostListPath = ( + props: { name: 'default' | 'hostList' } & HostIndexUIQueryParams, search?: string ) => { const { name, ...queryParams } = props; @@ -45,29 +45,27 @@ export const getEndpointListPath = ( ); const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`; - if (name === 'endpointList') { - return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { - tabName: ManagementSubTab.endpoints, + if (name === 'hostList') { + return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, { + tabName: ManagementSubTab.hosts, })}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; } return `${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; }; -export const getEndpointDetailsPath = ( - props: { name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps, +export const getHostDetailsPath = ( + props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps, search?: string ) => { const { name, ...queryParams } = props; - queryParams.show = (props.name === 'endpointPolicyResponse' + queryParams.show = (props.name === 'hostPolicyResponse' ? 'policy_response' : '') as HostIndexUIQueryParams['show']; - const urlQueryParams = querystringStringify( - queryParams - ); + const urlQueryParams = querystringStringify(queryParams); const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`; - return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { - tabName: ManagementSubTab.endpoints, + return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, { + tabName: ManagementSubTab.hosts, })}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; }; diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index 5dd47d4e88028..6486b1f3be6d1 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -16,6 +16,7 @@ import { EuiSelectable, EuiSelectableMessage, EuiSelectableProps, + EuiIcon, EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -35,75 +36,125 @@ const PolicyEmptyState = React.memo<{ onActionClick: (event: MouseEvent) => void; actionDisabled?: boolean; }>(({ loading, onActionClick, actionDisabled }) => { - const policySteps = useMemo( - () => [ - { - title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', { - defaultMessage: 'Head over to Ingest Manager.', - }), - children: ( - - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', { - defaultMessage: 'We’ll create a recommended security policy for you.', - }), - children: ( - - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', { - defaultMessage: 'Enroll your agents through Fleet.', - }), - children: ( - - - - ), - }, - ], - [] - ); - return ( - - } - bodyComponent={ - - } - /> +
+ {loading ? ( + + + + + + ) : ( + + + +

+ +

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

+ +

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

+ +

+
+
+
+ + + + +
+
+ + + + + + + + + + + + +
+ + + +
+ )} +
); }); -const EndpointsEmptyState = React.memo<{ +const HostsEmptyState = React.memo<{ loading: boolean; onActionClick: (event: MouseEvent) => void; actionDisabled: boolean; @@ -113,18 +164,18 @@ const EndpointsEmptyState = React.memo<{ const policySteps = useMemo( () => [ { - title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepOneTitle', { - defaultMessage: 'Select a policy you created from the list below.', + title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepOneTitle', { + defaultMessage: 'Select the policy you want to use to protect your hosts', }), children: ( <> - + - + @@ -146,7 +197,7 @@ const EndpointsEmptyState = React.memo<{ list ) : ( ); @@ -156,40 +207,56 @@ const EndpointsEmptyState = React.memo<{ ), }, { - title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepTwoTitle', { + title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepTwoTitle', { defaultMessage: - 'Head over to Ingest to deploy your Agent with Endpoint Security enabled.', + 'Enroll your agents enabled with Endpoint Security through Ingest Manager', }), + status: actionDisabled ? 'disabled' : '', children: ( - - - + + + + + + + + + + + + ), }, ], - [selectionOptions, handleSelectableOnChange, loading] + [selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick] ); return ( } bodyComponent={ } /> @@ -198,80 +265,45 @@ const EndpointsEmptyState = React.memo<{ const ManagementEmptyState = React.memo<{ loading: boolean; - onActionClick?: (event: MouseEvent) => void; - actionDisabled?: boolean; - actionButton?: JSX.Element; dataTestSubj: string; steps?: ManagementStep[]; headerComponent: JSX.Element; bodyComponent: JSX.Element; -}>( - ({ - loading, - onActionClick, - actionDisabled, - dataTestSubj, - steps, - actionButton, - headerComponent, - bodyComponent, - }) => { - return ( -
- {loading ? ( - - - - - - ) : ( - <> - - -

{headerComponent}

-
- - - {bodyComponent} - - - {steps && ( - - - - - - )} +}>(({ loading, dataTestSubj, steps, headerComponent, bodyComponent }) => { + return ( +
+ {loading ? ( + + + + + + ) : ( + <> + + +

{headerComponent}

+
+ + + {bodyComponent} + + + {steps && ( - <> - {actionButton ? ( - actionButton - ) : ( - - - - )} - + - - )} -
- ); - } -); + )} + + )} +
+ ); +}); PolicyEmptyState.displayName = 'PolicyEmptyState'; -EndpointsEmptyState.displayName = 'EndpointsEmptyState'; +HostsEmptyState.displayName = 'HostsEmptyState'; ManagementEmptyState.displayName = 'ManagementEmptyState'; -export { PolicyEmptyState, EndpointsEmptyState, ManagementEmptyState }; +export { PolicyEmptyState, HostsEmptyState }; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index c3dbb93b369a9..8495628709d2a 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -11,7 +11,7 @@ import { PageView, PageViewProps } from '../../common/components/endpoint/page_v import { ManagementSubTab } from '../types'; import { SecurityPageName } from '../../app/types'; import { useFormatUrl } from '../../common/components/link_to'; -import { getEndpointListPath, getPoliciesPath } from '../common/routing'; +import { getHostListPath, getPoliciesPath } from '../common/routing'; import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; export const ManagementPageView = memo>((options) => { @@ -19,7 +19,7 @@ export const ManagementPageView = memo>((options) => const { tabName } = useParams<{ tabName: ManagementSubTab }>(); const goToEndpoint = useNavigateByRouterEventHandler( - getEndpointListPath({ name: 'endpointList' }, search) + getHostListPath({ name: 'hostList' }, search) ); const goToPolicies = useNavigateByRouterEventHandler(getPoliciesPath(search)); @@ -31,11 +31,11 @@ export const ManagementPageView = memo>((options) => return [ { name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', { - defaultMessage: 'Endpoints', + defaultMessage: 'Hosts', }), - id: ManagementSubTab.endpoints, - isSelected: tabName === ManagementSubTab.endpoints, - href: formatUrl(getEndpointListPath({ name: 'endpointList' })), + id: ManagementSubTab.hosts, + isSelected: tabName === ManagementSubTab.hosts, + href: formatUrl(getHostListPath({ name: 'hostList' })), onClick: goToEndpoint, }, { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx index ff7f522b9bc52..a970edd4d30f4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx @@ -7,19 +7,19 @@ import { Switch, Route } from 'react-router-dom'; import React, { memo } from 'react'; import { HostList } from './view'; -import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants'; +import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; /** - * Provides the routing container for the endpoints related views + * Provides the routing container for the hosts related views */ -export const EndpointsContainer = memo(() => { +export const HostsContainer = memo(() => { return ( - + ); }); -EndpointsContainer.displayName = 'EndpointsContainer'; +HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts index ae2ce9facc837..533b14e50f3dd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts @@ -24,7 +24,7 @@ import { MiddlewareActionSpyHelper, createSpyMiddleware, } from '../../../../common/store/test_utils'; -import { getEndpointListPath } from '../../../common/routing'; +import { getHostListPath } from '../../../common/routing'; describe('host list pagination: ', () => { let fakeCoreStart: jest.Mocked; @@ -56,7 +56,7 @@ describe('host list pagination: ', () => { queryParams = () => uiQueryParams(store.getState()); historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { - return history.push(getEndpointListPath({ name: 'endpointList', ...nextQueryParams })); + return history.push(getHostListPath({ name: 'hostList', ...nextQueryParams })); }; }); @@ -70,7 +70,7 @@ describe('host list pagination: ', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: getEndpointListPath({ name: 'endpointList' }), + pathname: getHostListPath({ name: 'hostList' }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index e62c53e061a33..1c5c4fbac51ba 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -21,7 +21,7 @@ import { listData } from './selectors'; import { HostState } from '../types'; import { hostListReducer } from './reducer'; import { hostMiddlewareFactory } from './middleware'; -import { getEndpointListPath } from '../../../common/routing'; +import { getHostListPath } from '../../../common/routing'; describe('host list middleware', () => { let fakeCoreStart: jest.Mocked; @@ -60,7 +60,7 @@ describe('host list middleware', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: getEndpointListPath({ name: 'endpointList' }), + pathname: getHostListPath({ name: 'hostList' }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index e75d2129f61a5..4f47eaf565d8c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -15,7 +15,7 @@ import { HostPolicyResponseActionStatus, } from '../../../../../common/endpoint/types'; import { HostState, HostIndexUIQueryParams } from '../types'; -import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants'; +import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../../common/constants'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -114,7 +114,7 @@ export const policyResponseError = (state: Immutable) => state.policy export const isOnHostPage = (state: Immutable) => { return ( matchPath(state.location?.pathname ?? '', { - path: MANAGEMENT_ROUTING_ENDPOINTS_PATH, + path: MANAGEMENT_ROUTING_HOSTS_PATH, exact: true, }) !== null ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index 66abf993770a7..10ea271139e49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -26,7 +26,7 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; -import { getEndpointDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; +import { getHostDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public'; @@ -84,14 +84,14 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { selected_host, show, ...currentUrlParams } = queryParams; return [ formatUrl( - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', + getHostDetailsPath({ + name: 'hostPolicyResponse', ...currentUrlParams, selected_host: details.host.id, }) ), - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', + getHostDetailsPath({ + name: 'hostPolicyResponse', ...currentUrlParams, selected_host: details.host.id, }), @@ -108,7 +108,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { onDoneNavigateTo: [ 'securitySolution:management', { - path: getEndpointDetailsPath({ name: 'endpointDetails', selected_host: details.host.id }), + path: getHostDetailsPath({ name: 'hostDetails', selected_host: details.host.id }), }, ], }, @@ -200,8 +200,8 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { description: details.host.hostname, }, { - title: i18n.translate('xpack.securitySolution.endpoint.host.details.sensorVersion', { - defaultMessage: 'Sensor Version', + title: i18n.translate('xpack.securitySolution.endpoint.host.details.endpointVersion', { + defaultMessage: 'Endpoint Version', }), description: details.agent.version, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 3d44b73858e90..e29d796325bd6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -38,7 +38,7 @@ import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/endpoint/types'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { getEndpointListPath } from '../../../../common/routing'; +import { getHostListPath } from '../../../../common/routing'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -122,14 +122,14 @@ const PolicyResponseFlyoutPanel = memo<{ const [detailsUri, detailsRoutePath] = useMemo( () => [ formatUrl( - getEndpointListPath({ - name: 'endpointList', + getHostListPath({ + name: 'hostList', ...queryParams, selected_host: hostMeta.host.id, }) ), - getEndpointListPath({ - name: 'endpointList', + getHostListPath({ + name: 'hostList', ...queryParams, selected_host: hostMeta.host.id, }), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index b048a8f69b5d2..d11335df875e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -9,14 +9,14 @@ import { useMemo } from 'react'; import { useKibana } from '../../../../common/lib/kibana'; import { HostState } from '../types'; import { - MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_HOSTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, } from '../../../common/constants'; import { State } from '../../../../common/store'; export function useHostSelector(selector: (state: HostState) => TSelected) { return useSelector(function (state: State) { return selector( - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE] as HostState + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE] as HostState ); }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9766cd6abd2b1..996b987ea2be3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -44,7 +44,7 @@ describe('when on the hosts page', () => { it('should show the empty state when there are no hosts or polices', async () => { const renderResult = render(); - // Initially, there are no endpoints or policies, so we prompt to add policies first. + // Initially, there are no hosts or policies, so we prompt to add policies first. const table = await renderResult.findByTestId('emptyPolicyTable'); expect(table).not.toBeNull(); }); @@ -79,8 +79,8 @@ describe('when on the hosts page', () => { it('should show the no hosts empty state', async () => { const renderResult = render(); - const emptyEndpointsTable = await renderResult.findByTestId('emptyEndpointsTable'); - expect(emptyEndpointsTable).not.toBeNull(); + const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); + expect(emptyHostsTable).not.toBeNull(); }); it('should display the onboarding steps', async () => { @@ -335,7 +335,7 @@ describe('when on the hosts page', () => { const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( - '/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response' + '/hosts?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); @@ -549,7 +549,7 @@ describe('when on the hosts page', () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); expect(subHeaderBackLink.getAttribute('href')).toBe( - '/endpoints?page_index=0&page_size=10&selected_host=1' + '/hosts?page_index=0&page_size=10&selected_host=1' ); }); 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 d49335ca8de2c..8edeab15d6a09 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 @@ -10,10 +10,15 @@ import { EuiBasicTable, EuiBasicTableColumn, EuiText, + EuiTitle, + EuiSpacer, EuiLink, EuiHealth, EuiToolTip, EuiSelectableProps, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; @@ -33,7 +38,7 @@ import { CreateStructuredSelector } from '../../../../common/store'; import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { ManagementPageView } from '../../../components/management_page_view'; -import { PolicyEmptyState, EndpointsEmptyState } from '../../../components/management_empty_state'; +import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { @@ -41,11 +46,7 @@ import { AgentConfigDetailsDeployAgentAction, } from '../../../../../../ingest_manager/public'; import { SecurityPageName } from '../../../../app/types'; -import { - getEndpointListPath, - getEndpointDetailsPath, - getPolicyDetailPath, -} from '../../../common/routing'; +import { getHostListPath, getHostDetailsPath, getPolicyDetailPath } from '../../../common/routing'; import { useFormatUrl } from '../../../../common/components/link_to'; import { HostAction } from '../store/action'; @@ -107,8 +108,8 @@ export const HostList = () => { const { index, size } = page; // FIXME: PT: if host details is open, table is not displaying correct number of rows history.push( - getEndpointListPath({ - name: 'endpointList', + getHostListPath({ + name: 'hostList', ...queryParams, page_index: JSON.stringify(index), page_size: JSON.stringify(size), @@ -127,12 +128,12 @@ export const HostList = () => { state: { onCancelNavigateTo: [ 'securitySolution:management', - { path: getEndpointListPath({ name: 'endpointList' }) }, + { path: getHostListPath({ name: 'hostList' }) }, ], - onCancelUrl: formatUrl(getEndpointListPath({ name: 'endpointList' })), + onCancelUrl: formatUrl(getHostListPath({ name: 'hostList' })), onSaveNavigateTo: [ 'securitySolution:management', - { path: getEndpointListPath({ name: 'endpointList' }) }, + { path: getHostListPath({ name: 'hostList' }) }, ], }, } @@ -145,7 +146,7 @@ export const HostList = () => { state: { onDoneNavigateTo: [ 'securitySolution:management', - { path: getEndpointListPath({ name: 'endpointList' }) }, + { path: getHostListPath({ name: 'hostList' }) }, ], }, }); @@ -191,10 +192,10 @@ export const HostList = () => { defaultMessage: 'Hostname', }), render: ({ hostname, id }: HostInfo['metadata']['host']) => { - const toRoutePath = getEndpointDetailsPath( + const toRoutePath = getHostDetailsPath( { ...queryParams, - name: 'endpointDetails', + name: 'hostDetails', selected_host: id, }, search @@ -259,8 +260,8 @@ export const HostList = () => { }), // eslint-disable-next-line react/display-name render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { - const toRoutePath = getEndpointDetailsPath({ - name: 'endpointPolicyResponse', + const toRoutePath = getHostDetailsPath({ + name: 'hostPolicyResponse', selected_host: item.metadata.host.id, }); const toRouteUrl = formatUrl(toRoutePath); @@ -341,7 +342,7 @@ export const HostList = () => { ); } else if (!policyItemsLoading && policyItems && policyItems.length > 0) { return ( - { + + + +

+ +

+
+
+ + + +
+ + +

+ +

+
+ + } > {hasSelectedHost && } {listData && listData.length > 0 && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx new file mode 100644 index 0000000000000..5ec42671ec3d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.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 from 'react'; + +import { ManagementContainer } from './index'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; + +jest.mock('../../common/hooks/endpoint/ingest_enabled'); + +describe('when in the Admistration tab', () => { + let render: () => ReturnType; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + render = () => mockedContext.render(); + }); + + it('should display the No Permissions view when Ingest is OFF', async () => { + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + const renderResult = render(); + const noIngestPermissions = await renderResult.findByTestId('noIngestPermissions'); + expect(noIngestPermissions).not.toBeNull(); + }); + + it('should display the Management view when Ingest is ON', async () => { + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + const renderResult = render(); + const hostPage = await renderResult.findByTestId('hostPage'); + expect(hostPage).not.toBeNull(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 0e81b75d651ba..30800234ab24c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -7,27 +7,69 @@ import React, { memo } from 'react'; import { useHistory, Route, Switch } from 'react-router-dom'; +import { EuiText, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { PolicyContainer } from './policy'; import { - MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_HOSTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_ROOT_PATH, } from '../common/constants'; import { NotFoundPage } from '../../app/404'; -import { EndpointsContainer } from './endpoint_hosts'; -import { getEndpointListPath } from '../common/routing'; +import { HostsContainer } from './endpoint_hosts'; +import { getHostListPath } from '../common/routing'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../app/types'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; + +const NoPermissions = memo(() => { + return ( + <> + + } + body={ +

+ + + +

+ } + /> + + + ); +}); +NoPermissions.displayName = 'NoPermissions'; export const ManagementContainer = memo(() => { const history = useHistory(); + const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + + if (!isIngestEnabled) { + return ; + } + return ( - + { - history.replace(getEndpointListPath({ name: 'endpointList' })); + history.replace(getHostListPath({ name: 'hostList' })); return null; }} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 0bd623b27f4fb..102fd40c97672 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -43,8 +43,8 @@ describe('policy details: ', () => { config: { artifact_manifest: { value: { - manifest_version: 'v0', - schema_version: '1.0.0', + manifest_version: 'WzAsMF0=', + schema_version: 'v1', artifacts: {}, }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index db622ceb87b63..047aa6918736e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -37,9 +37,9 @@ describe('when on the policies page', () => { expect(table).not.toBeNull(); }); - it('should display the onboarding steps', async () => { + it('should display the instructions', async () => { const renderResult = render(); - const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); + const onboardingSteps = await renderResult.findByTestId('policyOnboardingInstructions'); expect(onboardingSteps).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 447a70ef998a9..8a77264c354ad 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -8,6 +8,8 @@ import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from import { EuiBasicTable, EuiText, + EuiTitle, + EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiTableFieldDataColumnType, @@ -20,8 +22,9 @@ import { EuiOverlayMask, EuiConfirmModal, EuiCallOut, - EuiSpacer, EuiButton, + EuiBetaBadge, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -391,9 +394,38 @@ export const PolicyList = React.memo(() => { + + + +

+ +

+
+
+ + + +
+ + +

+ +

+
+ + } headerRight={ { /> } - bodyHeader={ - policyItems && - policyItems.length > 0 && ( - + > + {policyItems && policyItems.length > 0 && ( + <> + - ) - } - > + + + )} {useMemo(() => { return ( <> diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts index 9ca170cce8b3d..a29da9bef5875 100644 --- a/x-pack/plugins/security_solution/public/management/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts @@ -12,7 +12,7 @@ import { import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; import { - MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_HOSTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, @@ -24,7 +24,7 @@ const policyListSelector = (state: State) => const policyDetailsSelector = (state: State) => state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]; const endpointsSelector = (state: State) => - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]; + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE]; export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( coreStart, diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 2ed3dfe86d2f8..f3c470fb1e8a3 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -14,7 +14,7 @@ import { initialPolicyListState, } from '../pages/policy/store/policy_list/reducer'; import { - MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_HOSTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, } from '../common/constants'; @@ -31,7 +31,7 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers; export const mockManagementState: Immutable = { [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), - [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialHostListState, + [MANAGEMENT_STORE_HOSTS_NAMESPACE]: initialHostListState, }; /** @@ -40,5 +40,5 @@ export const mockManagementState: Immutable = { export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, - [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer, + [MANAGEMENT_STORE_HOSTS_NAMESPACE]: hostListReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 854e9faa0204d..cb21a236ddd7e 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -18,14 +18,14 @@ export type ManagementStoreGlobalNamespace = 'management'; export type ManagementState = CombinedState<{ policyList: PolicyListState; policyDetails: PolicyDetailsState; - endpoints: HostState; + hosts: HostState; }>; /** * The management list of sub-tabs. Changes to these will impact the Router routes. */ export enum ManagementSubTab { - endpoints = 'endpoints', + hosts = 'hosts', policies = 'policy', } diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 1aa114608b479..d2d9861e0ae1a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -62,7 +62,7 @@ describe('Alerts by category', () => { test('it renders the expected title', () => { expect(wrapper.find('[data-test-subj="header-section-title"]').text()).toEqual( - 'External alert count' + 'External alert trend' ); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx index ee048f0d61212..3758bd10bfc8f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx @@ -7,13 +7,13 @@ import React, { memo } from 'react'; import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getEndpointListPath } from '../../../management/common/routing'; +import { getHostListPath } from '../../../management/common/routing'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url'; import { MANAGEMENT_APP_ID } from '../../../management/common/constants'; export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => { - const endpointsPath = getEndpointListPath({ name: 'endpointList' }); + const endpointsPath = getHostListPath({ name: 'hostList' }); const endpointsLink = useManagementFormatUrl(endpointsPath); const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, { path: endpointsPath, diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx index 8f2b3c7495f0d..4f9784b1f84bf 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx @@ -45,7 +45,7 @@ const StatefulRecentTimelinesComponent = React.memo( const { formatUrl } = useFormatUrl(SecurityPageName.timelines); const { navigateToApp } = useKibana().services.application; const onOpenTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + ({ duplicate, timelineId }) => { queryTimelineById({ apolloClient, duplicate, diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx index d91c2be214e8b..ddad72081645b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx @@ -20,6 +20,7 @@ import { OpenTimelineResult, } from '../../../timelines/components/open_timeline/types'; import { WithHoverActions } from '../../../common/components/with_hover_actions'; +import { TimelineType } from '../../../../common/types/timeline'; import { RecentTimelineCounts } from './counts'; import * as i18n from './translations'; @@ -58,9 +59,19 @@ export const RecentTimelines = React.memo<{ {showHoverContent && ( - + { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -128,6 +129,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -152,6 +154,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -171,6 +174,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -190,6 +194,27 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); + + test('it does NOT render the Endpoint banner when Ingest is NOT available', () => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); const wrapper = mount( 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 2a522d3ea8fde..6563f3c2b824d 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -29,6 +29,7 @@ import { SecurityPageName } from '../../app/types'; import { EndpointNotice } from '../components/endpoint_notice'; import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -64,6 +65,7 @@ const OverviewComponent: React.FC = ({ setDismissMessage(true); addMessage('management', 'dismissEndpointNotice'); }, [addMessage]); + const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); return ( <> @@ -74,7 +76,7 @@ const OverviewComponent: React.FC = ({ - {!dismissMessage && !metadataIndexExists && ( + {!dismissMessage && !metadataIndexExists && isIngestEnabled && ( <> diff --git a/x-pack/plugins/security_solution/public/overview/pages/translations.ts b/x-pack/plugins/security_solution/public/overview/pages/translations.ts index bf13a57f0b642..f62c643e9eae6 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/pages/translations.ts @@ -6,13 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const ALERTS_GRAPH_TITLE = i18n.translate( - 'xpack.securitySolution.overview.alertsGraphTitle', - { - defaultMessage: 'External alert count', - } -); - export const EVENTS = i18n.translate('xpack.securitySolution.overview.eventsTitle', { defaultMessage: 'Event count', }); @@ -47,7 +40,7 @@ export const RECENT_TIMELINES = i18n.translate( ); export const ALERT_COUNT = i18n.translate('xpack.securitySolution.overview.signalCountTitle', { - defaultMessage: 'Alert count', + defaultMessage: 'Detection alert trend', }); export const TOP = (fieldName: string) => diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 18072c25e6dde..6096a9b0e0bb8 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -34,7 +34,7 @@ import { import { APP_ID, APP_ICON, - APP_ALERTS_PATH, + APP_DETECTIONS_PATH, APP_HOSTS_PATH, APP_OVERVIEW_PATH, APP_NETWORK_PATH, @@ -48,6 +48,15 @@ import { ConfigureEndpointPackageConfig } from './management/pages/policy/view/i import { State, createStore, createInitialState } from './common/store'; import { SecurityPageName } from './app/types'; import { manageOldSiemRoutes } from './helpers'; +import { + OVERVIEW, + HOSTS, + NETWORK, + TIMELINES, + DETECTION_ENGINE, + CASE, + ADMINISTRATION, +} from './app/home/translations'; export class Plugin implements IPlugin { private kibanaVersion: string; @@ -95,10 +104,12 @@ export class Plugin implements IPlugin { + mount: async () => { const [{ application }] = await core.getStartServices(); application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); return () => true; @@ -107,9 +118,7 @@ export class Plugin implements IPlugin { const [ { coreStart, store, services, storage }, { renderApp, composeLibs }, - { alertsSubPlugin }, + { detectionsSubPlugin }, ] = await Promise.all([ mountSecurityFactory(), this.downloadAssets(), @@ -159,14 +166,14 @@ export class Plugin implements IPlugin = ( activeDescendantId: null, selectedDescendantId: null, processEntityIdOfSelectedDescendant: null, - panelToDisplay: null, }, action ) => { @@ -39,11 +38,6 @@ const uiReducer: Reducer = ( selectedDescendantId: action.payload.nodeId, processEntityIdOfSelectedDescendant: action.payload.selectedProcessId, }; - } else if (action.type === 'appDisplayedDifferentPanel') { - return { - ...uiState, - panelToDisplay: action.payload, - }; } else if ( action.type === 'userBroughtProcessIntoView' || action.type === 'appDetectedNewIdFromQueryParams' diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index e54193ab394a5..2bc254d118d33 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -127,11 +127,6 @@ export const uiSelectedDescendantProcessId = composeSelectors( uiSelectors.selectedDescendantProcessId ); -/** - * The current panel to display - */ -export const currentPanelView = composeSelectors(uiStateSelector, uiSelectors.currentPanelView); - /** * Returns the camera state from within ResolverState */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index bddc7d34abf1c..494d8884329c6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -39,8 +39,3 @@ export const selectedDescendantProcessId = createSelector( return processEntityIdOfSelectedDescendant; } ); - -// Select the current panel to be displayed -export const currentPanelView = (uiState: ResolverUIState) => { - return uiState.panelToDisplay; -}; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 5dd9a944b88ea..2025762a0605c 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -45,10 +45,6 @@ export interface ResolverUIState { * The entity_id of the process for the resolver's currently selected descendant. */ readonly processEntityIdOfSelectedDescendant: string | null; - /** - * Which panel the ui should display - */ - readonly panelToDisplay: string | null; } /** 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 2a2e7e87394a9..f4fe4fe520c92 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -4,17 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { - memo, - useCallback, - useMemo, - useContext, - useLayoutEffect, - useState, - useEffect, -} from 'react'; +import React, { memo, useCallback, useMemo, useContext, useLayoutEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; // eslint-disable-next-line import/no-nodejs-modules import querystring from 'querystring'; import { EuiPanel } from '@elastic/eui'; @@ -48,7 +40,7 @@ import { CrumbInfo } from './panels/panel_content_utilities'; */ const PanelContent = memo(function PanelContent() { const history = useHistory(); - const urlSearch = history.location.search; + const urlSearch = useLocation().search; const dispatch = useResolverDispatch(); const { timestamp } = useContext(SideEffectContext); @@ -205,21 +197,12 @@ const PanelContent = memo(function PanelContent() { return 'processListWithCounts'; }, [uiSelectedEvent, crumbEvent, crumbId, graphableProcessEntityIds]); - useEffect(() => { - // dispatch `appDisplayedDifferentPanel` to sync state with which panel gets displayed - dispatch({ - type: 'appDisplayedDifferentPanel', - payload: panelToShow, - }); - }, [panelToShow, dispatch]); - - const currentPanelView = useSelector(selectors.currentPanelView); const terminatedProcesses = useSelector(selectors.terminatedProcesses); const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined; const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false; const panelInstance = useMemo(() => { - if (currentPanelView === 'processDetails') { + if (panelToShow === 'processDetails') { return ( sum + val, 0); @@ -278,7 +261,7 @@ const PanelContent = memo(function PanelContent() { crumbId, pushToQueryParams, relatedStatsForIdFromParams, - currentPanelView, + panelToShow, isProcessTerminated, ]); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 56f88ccb13115..517b847855647 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -82,10 +82,13 @@ const invalidDateText = i18n.translate( } ); /** - * @param {ConstructorParameters[0]} timestamp To be passed through Date->Intl.DateTimeFormat * @returns {string} A nicely formatted string for a date */ -export function formatDate(timestamp: ConstructorParameters[0]) { +export function formatDate( + /** To be passed through Date->Intl.DateTimeFormat */ timestamp: ConstructorParameters< + typeof Date + >[0] +): string { const date = new Date(timestamp); if (isFinite(date.getTime())) { return formatter.format(date); diff --git a/x-pack/plugins/security_solution/public/sub_plugins.ts b/x-pack/plugins/security_solution/public/sub_plugins.ts index d47aae680aa35..5e7c5e8242fde 100644 --- a/x-pack/plugins/security_solution/public/sub_plugins.ts +++ b/x-pack/plugins/security_solution/public/sub_plugins.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alerts } from './alerts'; +import { Detections } from './detections'; import { Cases } from './cases'; import { Hosts } from './hosts'; import { Network } from './network'; @@ -12,7 +12,7 @@ import { Overview } from './overview'; import { Timelines } from './timelines'; import { Management } from './management'; -const alertsSubPlugin = new Alerts(); +const detectionsSubPlugin = new Detections(); const casesSubPlugin = new Cases(); const hostsSubPlugin = new Hosts(); const networkSubPlugin = new Network(); @@ -21,7 +21,7 @@ const timelinesSubPlugin = new Timelines(); const managementSubPlugin = new Management(); export { - alertsSubPlugin, + detectionsSubPlugin, casesSubPlugin, hostsSubPlugin, networkSubPlugin, diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx index f8d4a6eebcbff..195bb770312cb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx @@ -31,22 +31,26 @@ export const operatorLabels: EuiComboBoxOptionOption[] = [ }, ]; +export const EMPTY_ARRAY_RESULT = []; + /** Returns the names of fields in a category */ export const getFieldNames = (category: Partial): string[] => category.fields != null && Object.keys(category.fields).length > 0 ? Object.keys(category.fields) - : []; + : EMPTY_ARRAY_RESULT; /** Returns all field names by category, for display in an `EuiComboBox` */ export const getCategorizedFieldNames = (browserFields: BrowserFields): EuiComboBoxOptionOption[] => - Object.keys(browserFields) - .sort() - .map((categoryId) => ({ - label: categoryId, - options: getFieldNames(browserFields[categoryId]).map((fieldId) => ({ - label: fieldId, - })), - })); + !browserFields + ? EMPTY_ARRAY_RESULT + : Object.keys(browserFields) + .sort() + .map((categoryId) => ({ + label: categoryId, + options: getFieldNames(browserFields[categoryId]).map((fieldId) => ({ + label: fieldId, + })), + })); /** Returns true if the specified field name is valid */ export const selectionsAreValid = ({ @@ -61,7 +65,7 @@ export const selectionsAreValid = ({ const fieldId = selectedField.length > 0 ? selectedField[0].label : ''; const operator = selectedOperator.length > 0 ? selectedOperator[0].label : ''; - const fieldIsValid = getAllFieldsByName(browserFields)[fieldId] != null; + const fieldIsValid = browserFields && getAllFieldsByName(browserFields)[fieldId] != null; const operatorIsValid = findIndex((o) => o.label === operator, operatorLabels) !== -1; return fieldIsValid && operatorIsValid; diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx index 2160a05cb9da5..5d01995ac6380 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx @@ -9,7 +9,11 @@ import React from 'react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; -import { IS_OPERATOR, EXISTS_OPERATOR } from '../timeline/data_providers/data_provider'; +import { + DataProviderType, + IS_OPERATOR, + EXISTS_OPERATOR, +} from '../timeline/data_providers/data_provider'; import { StatefulEditDataProvider } from '.'; @@ -266,6 +270,27 @@ describe('StatefulEditDataProvider', () => { expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); }); + test('it does NOT render value when is template field', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); + }); + test('it does NOT disable the save button when field is valid', () => { const wrapper = mount( @@ -361,6 +386,7 @@ describe('StatefulEditDataProvider', () => { field: 'client.address', id: 'test', operator: ':', + type: 'default', providerId: 'hosts-table-hostName-test-host', value: 'test-host', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx index 95f3ec3b31649..72386a2b287f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; +import { noop, startsWith, endsWith } from 'lodash/fp'; import { EuiButton, EuiComboBox, @@ -17,12 +17,12 @@ import { EuiSpacer, EuiToolTip, } from '@elastic/eui'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; import { OnDataProviderEdited } from '../timeline/events'; -import { QueryOperator } from '../timeline/data_providers/data_provider'; +import { DataProviderType, QueryOperator } from '../timeline/data_providers/data_provider'; import { getCategorizedFieldNames, @@ -56,6 +56,7 @@ interface Props { providerId: string; timelineId: string; value: string | number; + type?: DataProviderType; } const sanatizeValue = (value: string | number): string => @@ -83,6 +84,7 @@ export const StatefulEditDataProvider = React.memo( providerId, timelineId, value, + type = DataProviderType.default, }) => { const [updatedField, setUpdatedField] = useState([{ label: field }]); const [updatedOperator, setUpdatedOperator] = useState( @@ -105,11 +107,18 @@ export const StatefulEditDataProvider = React.memo( } }; - const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionOption[]) => { - setUpdatedField(selectedField); + const onFieldSelected = useCallback( + (selectedField: EuiComboBoxOptionOption[]) => { + setUpdatedField(selectedField); - focusInput(); - }, []); + if (type === DataProviderType.template) { + setUpdatedValue(`{${selectedField[0].label}}`); + } + + focusInput(); + }, + [type] + ); const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionOption[]) => { setUpdatedOperator(operatorSelected); @@ -139,6 +148,36 @@ export const StatefulEditDataProvider = React.memo( window.onscroll = () => noop; }; + const handleSave = useCallback(() => { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + type, + }); + }, [ + onDataProviderEdited, + andProviderId, + updatedOperator, + updatedField, + timelineId, + providerId, + updatedValue, + type, + ]); + + const isValueFieldInvalid = useMemo( + () => + type !== DataProviderType.template && + (startsWith('{', sanatizeValue(updatedValue)) || + endsWith('}', sanatizeValue(updatedValue))), + [type, updatedValue] + ); + useEffect(() => { disableScrolling(); focusInput(); @@ -190,7 +229,8 @@ export const StatefulEditDataProvider = React.memo( - {updatedOperator.length > 0 && + {type !== DataProviderType.template && + updatedOperator.length > 0 && updatedOperator[0].label !== i18n.EXISTS && updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( @@ -201,6 +241,7 @@ export const StatefulEditDataProvider = React.memo( onChange={onValueChange} placeholder={i18n.VALUE} value={sanatizeValue(updatedValue)} + isInvalid={isValueFieldInvalid} /> @@ -224,19 +265,9 @@ export const StatefulEditDataProvider = React.memo( browserFields, selectedField: updatedField, selectedOperator: updatedOperator, - }) + }) || isValueFieldInvalid } - onClick={() => { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} + onClick={handleSave} size="s" > {i18n.SAVE} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx index a1392ad8b8270..5896a02b82023 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx @@ -124,12 +124,13 @@ export const FlyoutButton = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 8e34e11e85729..10f20eeacbcb0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -9,10 +9,10 @@ import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import { isEmpty, get } from 'lodash/fp'; +import { TimelineType } from '../../../../../common/types/timeline'; import { History } from '../../../../common/lib/history'; import { Note } from '../../../../common/lib/note'; import { appSelectors, inputsModel, inputsSelectors, State } from '../../../../common/store'; -import { defaultHeaders } from '../../timeline/body/column_headers/default_headers'; import { Properties } from '../../timeline/properties'; import { appActions } from '../../../../common/store/app'; import { inputsActions } from '../../../../common/store/inputs'; @@ -31,7 +31,6 @@ type Props = OwnProps & PropsFromRedux; const StatefulFlyoutHeader = React.memo( ({ associateNote, - createTimeline, description, graphEventId, isDataInTimeline, @@ -57,7 +56,6 @@ const StatefulFlyoutHeader = React.memo( return ( { title = '', noteIds = emptyNotesId, status, - timelineType, + timelineType = TimelineType.default, } = timeline; const history = emptyHistory; // TODO: get history from store via selector @@ -127,14 +125,6 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - createTimeline: ({ id, show }: { id: string; show?: boolean }) => - dispatch( - timelineActions.createTimeline({ - id, - columns: defaultHeaders, - show, - }) - ), updateDescription: ({ id, description }: { id: string; description: string }) => dispatch(timelineActions.updateDescription({ id, description })), updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx index 15c078e175355..27fda48b69598 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx @@ -7,7 +7,7 @@ import { EuiContextMenuPanel, EuiContextMenuItem, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import * as i18n from './translations'; import { DeleteTimelines, OpenTimelineResult } from './types'; @@ -26,10 +26,12 @@ export const useEditTimelineBatchActions = ({ deleteTimelines, selectedItems, tableRef, + timelineType = TimelineType.default, }: { deleteTimelines?: DeleteTimelines; selectedItems?: OpenTimelineResult[]; tableRef: React.MutableRefObject | undefined>; + timelineType: TimelineType | null; }) => { const { enableExportTimelineDownloader, @@ -49,8 +51,7 @@ export const useEditTimelineBatchActions = ({ disableExportTimelineDownloader(); onCloseDeleteTimelineModal(); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [disableExportTimelineDownloader, onCloseDeleteTimelineModal, tableRef.current] + [disableExportTimelineDownloader, onCloseDeleteTimelineModal, tableRef] ); const selectedIds = useMemo(() => getExportedIds(selectedItems ?? []), [selectedItems]); @@ -76,7 +77,9 @@ export const useEditTimelineBatchActions = ({ onComplete={onCompleteBatchActions.bind(null, closePopover)} title={ selectedItems?.length !== 1 - ? i18n.SELECTED_TIMELINES(selectedItems?.length ?? 0) + ? timelineType === TimelineType.template + ? i18n.SELECTED_TEMPLATES(selectedItems?.length ?? 0) + : i18n.SELECTED_TIMELINES(selectedItems?.length ?? 0) : selectedItems[0]?.title ?? '' } /> @@ -106,14 +109,15 @@ export const useEditTimelineBatchActions = ({ }, // eslint-disable-next-line react-hooks/exhaustive-deps [ + selectedItems, deleteTimelines, + selectedIds, isEnableDownloader, isDeleteTimelineModalOpen, - selectedIds, - selectedItems, + onCompleteBatchActions, + timelineType, handleEnableExportTimelineDownloader, handleOnOpenDeleteTimelineModal, - onCompleteBatchActions, ] ); return { onCompleteBatchActions, getBatchItemsPopoverContent }; 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 e841718c8119b..03a6d475b3426 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 @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable complexity */ + import ApolloClient from 'apollo-client'; import { getOr, set, isEmpty } from 'lodash/fp'; import { Action } from 'typescript-fsa'; import uuid from 'uuid'; import { Dispatch } from 'redux'; +import deepMerge from 'deepmerge'; import { oneTimelineQuery } from '../../containers/one/index.gql_query'; import { TimelineResult, @@ -17,9 +20,10 @@ import { FilterTimelineResult, ColumnHeaderResult, PinnedEvent, + DataProviderResult, } from '../../../graphql/types'; -import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { DataProviderType, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; import { addNotes as dispatchAddNotes, @@ -47,6 +51,7 @@ import { import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; import { getTimeRangeSettings } from '../../../common/utils/default_date_settings'; import { createNote } from '../notes/helpers'; +import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -162,15 +167,61 @@ const setPinnedEventIds = (duplicate: boolean, pinnedEventIds: string[] | null | ? pinnedEventIds.reduce((acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), {}) : {}; +const getTemplateTimelineId = ( + timeline: TimelineResult, + duplicate: boolean, + targetTimelineType?: TimelineType +) => { + if (!duplicate) { + return timeline.templateTimelineId; + } + + if ( + targetTimelineType === TimelineType.default && + timeline.timelineType === TimelineType.template + ) { + return timeline.templateTimelineId; + } + + // TODO: MOVE TO BACKEND + return uuid.v4(); +}; + +const convertToDefaultField = ({ and, ...dataProvider }: DataProviderResult) => + deepMerge(dataProvider, { + type: DataProviderType.default, + queryMatch: { + value: + dataProvider.queryMatch!.operator === IS_OPERATOR ? '' : dataProvider.queryMatch!.value, + }, + }); + +const getDataProviders = ( + duplicate: boolean, + dataProviders: TimelineResult['dataProviders'], + timelineType?: TimelineType +) => { + if (duplicate && dataProviders && timelineType === TimelineType.default) { + return dataProviders.map((dataProvider) => ({ + ...convertToDefaultField(dataProvider), + and: dataProvider.and?.map(convertToDefaultField) ?? [], + })); + } + + return dataProviders; +}; + // eslint-disable-next-line complexity export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, - duplicate: boolean + duplicate: boolean, + timelineType?: TimelineType ): TimelineModel => { const isTemplate = timeline.timelineType === TimelineType.template; const timelineEntries = { ...timeline, columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders, + dataProviders: getDataProviders(duplicate, timeline.dataProviders, timelineType), eventIdToNoteIds: setEventIdToNoteIds(duplicate, timeline.eventIdToNoteIds), filters: timeline.filters != null ? timeline.filters.map(setTimelineFilters) : [], isFavorite: duplicate @@ -185,8 +236,9 @@ export const defaultTimelineToTimelineModel = ( status: duplicate ? TimelineStatus.active : timeline.status, savedObjectId: duplicate ? null : timeline.savedObjectId, version: duplicate ? null : timeline.version, + timelineType: timelineType ?? timeline.timelineType, title: duplicate ? `${timeline.title} - Duplicate` : timeline.title || '', - templateTimelineId: duplicate && isTemplate ? uuid.v4() : timeline.templateTimelineId, + templateTimelineId: getTemplateTimelineId(timeline, duplicate, timelineType), templateTimelineVersion: duplicate && isTemplate ? 1 : timeline.templateTimelineVersion, }; return Object.entries(timelineEntries).reduce( @@ -200,12 +252,13 @@ export const defaultTimelineToTimelineModel = ( export const formatTimelineResultToModel = ( timelineToOpen: TimelineResult, - duplicate: boolean = false + duplicate: boolean = false, + timelineType?: TimelineType ): { notes: NoteResult[] | null | undefined; timeline: TimelineModel } => { const { notes, ...timelineModel } = timelineToOpen; return { notes, - timeline: defaultTimelineToTimelineModel(timelineModel, duplicate), + timeline: defaultTimelineToTimelineModel(timelineModel, duplicate, timelineType), }; }; @@ -214,6 +267,7 @@ export interface QueryTimelineById { duplicate?: boolean; graphEventId?: string; timelineId: string; + timelineType?: TimelineType; onOpenTimeline?: (timeline: TimelineModel) => void; openTimeline?: boolean; updateIsLoading: ({ @@ -231,6 +285,7 @@ export const queryTimelineById = ({ duplicate = false, graphEventId = '', timelineId, + timelineType, onOpenTimeline, openTimeline = true, updateIsLoading, @@ -250,7 +305,11 @@ export const queryTimelineById = ({ getOr({}, 'data.getOneTimeline', result) ); - const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate); + const { timeline, notes } = formatTimelineResultToModel( + timelineToOpen, + duplicate, + timelineType + ); if (onOpenTimeline != null) { onOpenTimeline(timeline); } else if (updateTimeline) { 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 ea63f2b7b0710..6d332c79f77cd 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 @@ -7,11 +7,8 @@ import ApolloClient from 'apollo-client'; import React, { useEffect, useState, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; - import { Dispatch } from 'redux'; -import { disableTemplate } from '../../../../common/constants'; - import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types'; import { State } from '../../../common/store'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; @@ -267,7 +264,7 @@ export const StatefulOpenTimelineComponent = React.memo( }, []); const openTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + ({ duplicate, timelineId, timelineType: timelineTypeToOpen }) => { if (isModal && closeModalTimeline != null) { closeModalTimeline(); } @@ -277,6 +274,7 @@ export const StatefulOpenTimelineComponent = React.memo( duplicate, onOpenTimeline, timelineId, + timelineType: timelineTypeToOpen, updateIsLoading, updateTimeline, }); @@ -318,9 +316,9 @@ export const StatefulOpenTimelineComponent = React.memo( selectedItems={selectedItems} sortDirection={sortDirection} sortField={sortField} - templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null} + templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} - timelineFilter={!disableTemplate ? timelineTabs : null} + timelineFilter={timelineTabs} title={title} totalSearchResultsCount={totalCount} /> @@ -348,9 +346,9 @@ export const StatefulOpenTimelineComponent = React.memo( selectedItems={selectedItems} sortDirection={sortDirection} sortField={sortField} - templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null} + templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} - timelineFilter={!disableTemplate ? timelineFilters : null} + timelineFilter={timelineFilters} title={title} totalSearchResultsCount={totalCount} /> 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 849143894efe0..60b009f59c13b 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,6 +8,7 @@ import { EuiPanel, EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { TimelineType } from '../../../../common/types/timeline'; import { ImportDataModal } from '../../../common/components/import_data_modal'; import { UtilityBarGroup, @@ -36,7 +37,6 @@ export const OpenTimeline = React.memo( isLoading, itemIdToExpandedNotesRowMap, importDataModalToggle, - onAddTimelinesToFavorites, onDeleteSelected, onlyFavorites, onOpenTimeline, @@ -54,7 +54,7 @@ export const OpenTimeline = React.memo( sortDirection, setImportDataModalToggle, sortField, - timelineType, + timelineType = TimelineType.default, timelineFilter, templateTimelineFilter, totalSearchResultsCount, @@ -73,8 +73,27 @@ export const OpenTimeline = React.memo( deleteTimelines, selectedItems, tableRef, + timelineType, }); + const nTemplates = useMemo( + () => ( + + {query.trim().length ? `${i18n.WITH} "${query.trim()}"` : ''} + + ), + }} + /> + ), + [totalSearchResultsCount, query] + ); + const nTimelines = useMemo( () => ( ( } }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); - const actionTimelineToShow = useMemo( - () => - onDeleteSelected != null && deleteTimelines != null - ? ['delete', 'duplicate', 'export', 'selectable'] - : ['duplicate', 'export', 'selectable'], - [onDeleteSelected, deleteTimelines] - ); + const actionTimelineToShow = useMemo(() => { + const timelineActions: ActionTimelineToShow[] = [ + 'createFrom', + 'duplicate', + 'export', + 'selectable', + ]; + + if (onDeleteSelected != null && deleteTimelines != null) { + timelineActions.push('delete'); + } + + return timelineActions; + }, [onDeleteSelected, deleteTimelines]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); @@ -167,7 +193,7 @@ export const OpenTimeline = React.memo( onQueryChange={onQueryChange} onToggleOnlyFavorites={onToggleOnlyFavorites} query={query} - totalSearchResultsCount={totalSearchResultsCount} + timelineType={timelineType} > {SearchRowContent} @@ -177,13 +203,18 @@ export const OpenTimeline = React.memo( <> - {i18n.SHOWING} {nTimelines} + {i18n.SHOWING}{' '} + {timelineType === TimelineType.template ? nTemplates : nTimelines} - {i18n.SELECTED_TIMELINES(selectedItems.length)} + + {timelineType === TimelineType.template + ? i18n.SELECTED_TEMPLATES(selectedItems.length) + : i18n.SELECTED_TIMELINES(selectedItems.length)} + ( totalSearchResultsCount, }) => { const actionsToShow = useMemo(() => { - const actions: ActionTimelineToShow[] = - onDeleteSelected != null && deleteTimelines != null - ? ['delete', 'duplicate'] - : ['duplicate']; + const actions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; + + if (onDeleteSelected != null && deleteTimelines != null) { + actions.push('delete'); + } + return actions.filter((action) => !hideActions.includes(action)); }, [onDeleteSelected, deleteTimelines, hideActions]); @@ -84,8 +86,8 @@ export const OpenTimelineModalBody = memo( onlyFavorites={onlyFavorites} onQueryChange={onQueryChange} onToggleOnlyFavorites={onToggleOnlyFavorites} - query={query} - totalSearchResultsCount={totalSearchResultsCount} + query="" + timelineType={timelineType} > {SearchRowContent} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx index 77aa306157c92..2e6dcb85ad769 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx @@ -10,6 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import { TimelineType } from '../../../../../common/types/timeline'; + import { SearchRow } from '.'; import * as i18n from '../translations'; @@ -25,7 +27,7 @@ describe('SearchRow', () => { onQueryChange={jest.fn()} onToggleOnlyFavorites={jest.fn()} query="" - totalSearchResultsCount={0} + timelineType={TimelineType.default} /> ); @@ -45,7 +47,7 @@ describe('SearchRow', () => { onQueryChange={jest.fn()} onToggleOnlyFavorites={jest.fn()} query="" - totalSearchResultsCount={0} + timelineType={TimelineType.default} /> ); @@ -65,7 +67,7 @@ describe('SearchRow', () => { onQueryChange={jest.fn()} onToggleOnlyFavorites={onToggleOnlyFavorites} query="" - totalSearchResultsCount={0} + timelineType={TimelineType.default} /> ); @@ -83,7 +85,7 @@ describe('SearchRow', () => { onQueryChange={jest.fn()} onToggleOnlyFavorites={jest.fn()} query="" - totalSearchResultsCount={0} + timelineType={TimelineType.default} /> ); @@ -104,7 +106,7 @@ describe('SearchRow', () => { onQueryChange={jest.fn()} onToggleOnlyFavorites={jest.fn()} query="" - totalSearchResultsCount={0} + timelineType={TimelineType.default} /> ); @@ -129,7 +131,7 @@ describe('SearchRow', () => { onQueryChange={onQueryChange} onToggleOnlyFavorites={jest.fn()} query="" - totalSearchResultsCount={32} + timelineType={TimelineType.default} /> ); 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 6f9178664ccf0..5b927db3c37a9 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 @@ -12,9 +12,10 @@ import { // @ts-ignore EuiSearchBar, } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { TimelineType } from '../../../../../common/types/timeline'; import * as i18n from '../translations'; import { OpenTimelineProps } from '../types'; @@ -39,14 +40,9 @@ type Props = Pick< | 'onQueryChange' | 'onToggleOnlyFavorites' | 'query' - | 'totalSearchResultsCount' + | 'timelineType' > & { children?: JSX.Element | null }; -const searchBox = { - placeholder: i18n.SEARCH_PLACEHOLDER, - incremental: false, -}; - /** * Renders the row containing the search input and Only Favorites filter */ @@ -56,10 +52,20 @@ export const SearchRow = React.memo( onlyFavorites, onQueryChange, onToggleOnlyFavorites, - query, - totalSearchResultsCount, children, + timelineType, }) => { + const searchBox = useMemo( + () => ({ + placeholder: + timelineType === TimelineType.default + ? i18n.SEARCH_PLACEHOLDER + : i18n.SEARCH_TEMPLATE_PLACEHOLDER, + incremental: false, + }), + [timelineType] + ); + return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx index 5b8eb8fd0365c..aa4bb3f1e0467 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx @@ -16,7 +16,7 @@ import { TimelineActionsOverflowColumns, } from '../types'; import * as i18n from '../translations'; -import { TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; /** * Returns the action columns (e.g. delete, open duplicate timeline) @@ -34,6 +34,42 @@ export const getActionsColumns = ({ onOpenDeleteTimelineModal?: OnOpenDeleteTimelineModal; onOpenTimeline: OnOpenTimeline; }): [TimelineActionsOverflowColumns] => { + const createTimelineFromTemplate = { + name: i18n.CREATE_TIMELINE_FROM_TEMPLATE, + icon: 'timeline', + onClick: ({ savedObjectId }: OpenTimelineResult) => { + onOpenTimeline({ + duplicate: true, + timelineType: TimelineType.default, + timelineId: savedObjectId!, + }); + }, + type: 'icon', + enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null, + description: i18n.CREATE_TIMELINE_FROM_TEMPLATE, + 'data-test-subj': 'create-from-template', + available: (item: OpenTimelineResult) => + item.timelineType === TimelineType.template && actionTimelineToShow.includes('createFrom'), + }; + + const createTemplateFromTimeline = { + name: i18n.CREATE_TEMPLATE_FROM_TIMELINE, + icon: 'visText', + onClick: ({ savedObjectId }: OpenTimelineResult) => { + onOpenTimeline({ + duplicate: true, + timelineType: TimelineType.template, + timelineId: savedObjectId!, + }); + }, + type: 'icon', + enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null, + description: i18n.CREATE_TEMPLATE_FROM_TIMELINE, + 'data-test-subj': 'create-template-from-timeline', + available: (item: OpenTimelineResult) => + item.timelineType !== TimelineType.template && actionTimelineToShow.includes('createFrom'), + }; + const openAsDuplicateColumn = { name: i18n.OPEN_AS_DUPLICATE, icon: 'copy', @@ -47,6 +83,25 @@ export const getActionsColumns = ({ enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null, description: i18n.OPEN_AS_DUPLICATE, 'data-test-subj': 'open-duplicate', + available: (item: OpenTimelineResult) => + item.timelineType !== TimelineType.template && actionTimelineToShow.includes('duplicate'), + }; + + const openAsDuplicateTemplateColumn = { + name: i18n.OPEN_AS_DUPLICATE_TEMPLATE, + icon: 'copy', + onClick: ({ savedObjectId }: OpenTimelineResult) => { + onOpenTimeline({ + duplicate: true, + timelineId: savedObjectId ?? '', + }); + }, + type: 'icon', + enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null, + description: i18n.OPEN_AS_DUPLICATE_TEMPLATE, + 'data-test-subj': 'open-duplicate-template', + available: (item: OpenTimelineResult) => + item.timelineType === TimelineType.template && actionTimelineToShow.includes('duplicate'), }; const exportTimelineAction = { @@ -60,6 +115,7 @@ export const getActionsColumns = ({ }, description: i18n.EXPORT_SELECTED, 'data-test-subj': 'export-timeline', + available: () => actionTimelineToShow.includes('export'), }; const deleteTimelineColumn = { @@ -72,18 +128,20 @@ export const getActionsColumns = ({ savedObjectId != null && status !== TimelineStatus.immutable, description: i18n.DELETE_SELECTED, 'data-test-subj': 'delete-timeline', + available: () => actionTimelineToShow.includes('delete') && deleteTimelines != null, }; return [ { - width: '40px', + width: '80px', actions: [ - actionTimelineToShow.includes('duplicate') ? openAsDuplicateColumn : null, - actionTimelineToShow.includes('export') ? exportTimelineAction : null, - actionTimelineToShow.includes('delete') && deleteTimelines != null - ? deleteTimelineColumn - : null, - ].filter((action) => action != null), + createTimelineFromTemplate, + createTemplateFromTimeline, + openAsDuplicateColumn, + openAsDuplicateTemplateColumn, + exportTimelineAction, + deleteTimelineColumn, + ], }, ]; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx index e0c7ab68f6bf5..eb9ddcce112d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx @@ -17,6 +17,7 @@ import * as i18n from '../translations'; import { OnOpenTimeline, OnToggleShowNotes, OpenTimelineResult } from '../types'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; +import { TimelineType } from '../../../../../common/types/timeline'; /** * Returns the column definitions (passed as the `columns` prop to @@ -27,10 +28,12 @@ export const getCommonColumns = ({ itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, + timelineType, }: { onOpenTimeline: OnOpenTimeline; onToggleShowNotes: OnToggleShowNotes; itemIdToExpandedNotesRowMap: Record; + timelineType: TimelineType | null; }) => [ { isExpander: true, @@ -55,7 +58,7 @@ export const getCommonColumns = ({ { dataType: 'string', field: 'title', - name: i18n.TIMELINE_NAME, + name: timelineType === TimelineType.default ? i18n.TIMELINE_NAME : i18n.TIMELINE_TEMPLATE_NAME, render: (title: string, timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null ? ( [ - { - dataType: 'string', - field: 'updatedBy', - name: i18n.MODIFIED_BY, - render: (updatedBy: OpenTimelineResult['updatedBy']) => ( -
{defaultToEmptyTag(updatedBy)}
- ), - sortable: false, - }, -]; +export const getExtendedColumns = (showExtendedColumns: boolean) => { + if (!showExtendedColumns) return []; + + return [ + { + dataType: 'string', + field: 'updatedBy', + name: i18n.MODIFIED_BY, + render: (updatedBy: OpenTimelineResult['updatedBy']) => ( +
{defaultToEmptyTag(updatedBy)}
+ ), + sortable: 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 fdba3247afb38..2c55edb9034b5 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 @@ -5,7 +5,7 @@ */ import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import * as i18n from '../translations'; @@ -40,9 +40,6 @@ const BasicTable = styled(EuiBasicTable)` `; BasicTable.displayName = 'BasicTable'; -const getExtendedColumnsIfEnabled = (showExtendedColumns: boolean) => - showExtendedColumns ? [...getExtendedColumns()] : []; - /** * Returns the column definitions (passed as the `columns` prop to * `EuiBasicTable`) that are displayed in the compact `Open Timeline` modal @@ -77,8 +74,9 @@ export const getTimelinesTableColumns = ({ itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, + timelineType, }), - ...getExtendedColumnsIfEnabled(showExtendedColumns), + ...getExtendedColumns(showExtendedColumns), ...getIconHeaderColumns({ timelineType }), ...getActionsColumns({ actionTimelineToShow, @@ -167,9 +165,10 @@ export const TimelinesTable = React.memo( onSelectionChange, }; const basicTableProps = tableRef != null ? { ref: tableRef } : {}; - return ( - + getTimelinesTableColumns({ actionTimelineToShow, deleteTimelines, itemIdToExpandedNotesRowMap, @@ -180,7 +179,24 @@ export const TimelinesTable = React.memo( onToggleShowNotes, showExtendedColumns, timelineType, - })} + }), + [ + actionTimelineToShow, + deleteTimelines, + itemIdToExpandedNotesRowMap, + enableExportTimelineDownloader, + onOpenDeleteTimelineModal, + onOpenTimeline, + onSelectionChange, + onToggleShowNotes, + showExtendedColumns, + timelineType, + ] + ); + + return ( + + i18n.translate('xpack.securitySolution.open.timeline.selectedTemplatesTitle', { + values: { selectedTemplates }, + defaultMessage: + 'Selected {selectedTemplates} {selectedTemplates, plural, =1 {template} other {templates}}', + }); + export const SELECTED_TIMELINES = (selectedTimelines: number) => i18n.translate('xpack.securitySolution.open.timeline.selectedTimelinesTitle', { values: { selectedTimelines }, @@ -298,6 +368,7 @@ export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message: export const TEMPLATE_CALL_OUT_MESSAGE = i18n.translate( 'xpack.securitySolution.timelines.components.templateCallOutMessageTitle', { - defaultMessage: 'Now you can add timeline templates and link it to rules.', + defaultMessage: + 'Prebuit detection rules are now packaged with Timeline templates. You can also create your own Timeline templates and associate them with any rule.', } ); 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 8811d5452e039..c21edaa916588 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 @@ -54,7 +54,7 @@ export interface OpenTimelineResult { status?: TimelineStatus | null; title?: string | null; templateTimelineId?: string | null; - type?: TimelineTypeLiteral; + timelineType?: TimelineTypeLiteral; updated?: number | null; updatedBy?: string | null; } @@ -82,9 +82,11 @@ export type OnDeleteOneTimeline = (timelineIds: string[]) => void; export type OnOpenTimeline = ({ duplicate, timelineId, + timelineType, }: { duplicate: boolean; timelineId: string; + timelineType?: TimelineTypeLiteral; }) => void; export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; @@ -117,7 +119,7 @@ export interface OnTableChangeParams { /** Invoked by the EUI table implementation when the user interacts with the table */ export type OnTableChange = (tableChange: OnTableChangeParams) => void; -export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; +export type ActionTimelineToShow = 'createFrom' | 'duplicate' | 'delete' | 'export' | 'selectable'; export interface OpenTimelineProps { /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ @@ -172,7 +174,7 @@ export interface OpenTimelineProps { timelineType: TimelineTypeLiteralWithNull; /** when timelineType === template, templatetimelineFilter is a JSX.Element */ templateTimelineFilter: JSX.Element[] | null; - /** timeline / template timeline */ + /** timeline / timeline template */ timelineFilter?: JSX.Element | JSX.Element[] | null; /** The title of the Open Timeline component */ title: string; 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 f17f6aebaddf6..c321caed46f22 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 @@ -17,7 +17,6 @@ import { import * as i18n from './translations'; import { TemplateTimelineFilter } from './types'; -import { disableTemplate } from '../../../../common/constants'; export const useTimelineStatus = ({ timelineType, @@ -33,16 +32,16 @@ export const useTimelineStatus = ({ templateTimelineFilter: JSX.Element[] | null; } => { const [selectedTab, setSelectedTab] = useState( - disableTemplate ? null : TemplateTimelineType.elastic + TemplateTimelineType.elastic ); const isTemplateFilterEnabled = useMemo(() => timelineType === TimelineType.template, [ timelineType, ]); - const templateTimelineType = useMemo( - () => (disableTemplate || !isTemplateFilterEnabled ? null : selectedTab), - [selectedTab, isTemplateFilterEnabled] - ); + const templateTimelineType = useMemo(() => (!isTemplateFilterEnabled ? null : selectedTab), [ + selectedTab, + isTemplateFilterEnabled, + ]); const timelineStatus = useMemo( () => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 7baefaa6ab951..e38f6ad022d78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -901,6 +901,7 @@ In other use cases the message field can be used to concatenate different values } indexToAdd={Array []} isLive={false} + isSaving={false} itemsPerPage={5} itemsPerPageOptions={ Array [ @@ -918,6 +919,7 @@ In other use cases the message field can be used to concatenate different values onDataProviderRemoved={[MockFunction]} onToggleDataProviderEnabled={[MockFunction]} onToggleDataProviderExcluded={[MockFunction]} + onToggleDataProviderType={[MockFunction]} show={true} showCallOutUnauthorizedMsg={false} sort={ @@ -928,6 +930,7 @@ In other use cases the message field can be used to concatenate different values } start={1521830963132} status="active" + timelineType="default" toggleColumn={[MockFunction]} usersViewing={ Array [ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index e8074c2f6f381..445f2d8e62c82 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -10,4 +10,6 @@ export const IP_FIELD_TYPE = 'ip'; export const MESSAGE_FIELD_NAME = 'message'; export const EVENT_MODULE_FIELD_NAME = 'event.module'; export const RULE_REFERENCE_FIELD_NAME = 'rule.reference'; +export const REFERENCE_URL_FIELD_NAME = 'reference.url'; +export const EVENT_URL_FIELD_NAME = 'event.url'; export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index b2588e19800a6..ab9e47f5ae3f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -29,8 +29,10 @@ import { EVENT_MODULE_FIELD_NAME, RULE_REFERENCE_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + EVENT_URL_FIELD_NAME, } from './constants'; -import { RenderRuleName, renderEventModule, renderRulReference } from './formatted_field_helpers'; +import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers'; // simple black-list to prevent dragging and dropping fields such as message name const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; @@ -107,8 +109,10 @@ const FormattedFieldValueComponent: React.FC<{ ); } else if (fieldName === EVENT_MODULE_FIELD_NAME) { return renderEventModule({ contextId, eventId, fieldName, linkValue, truncate, value }); - } else if (fieldName === RULE_REFERENCE_FIELD_NAME) { - return renderRulReference({ contextId, eventId, fieldName, linkValue, truncate, value }); + } else if ( + [RULE_REFERENCE_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME].includes(fieldName) + ) { + return renderUrl({ contextId, eventId, fieldName, linkValue, truncate, value }); } else if (columnNamesNotDraggable.includes(fieldName)) { return truncate && !isEmpty(value) ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx index 81820e2253fc9..8e64b484ffd2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -47,14 +47,14 @@ export const RenderRuleName: React.FC = ({ }) => { const ruleName = `${value}`; const ruleId = linkValue; - const { search } = useFormatUrl(SecurityPageName.alerts); + const { search } = useFormatUrl(SecurityPageName.detections); const { navigateToApp, getUrlForApp } = useKibana().services.application; const content = truncate ? {value} : value; const goToRuleDetails = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, { + navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { path: getRuleDetailsUrl(ruleId ?? '', search), }); }, @@ -70,7 +70,7 @@ export const RenderRuleName: React.FC = ({ > @@ -150,7 +150,7 @@ export const renderEventModule = ({ ); }; -export const renderRulReference = ({ +export const renderUrl = ({ contextId, eventId, fieldName, @@ -165,23 +165,23 @@ export const renderRulReference = ({ truncate?: boolean; value: string | number | null | undefined; }) => { - const referenceUrlName = `${value}`; + const urlName = `${value}`; const content = truncate ? {value} : value; - return isString(value) && referenceUrlName.length > 0 ? ( + return isString(value) && urlName.length > 0 ? ( - {!isUrlInvalid(referenceUrlName) && ( - + {!isUrlInvalid(urlName) && ( + {content} )} - {isUrlInvalid(referenceUrlName) && <>{content}} + {isUrlInvalid(urlName) && <>{content}} ) : ( getEmptyTagValue() diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap index 46a6970720def..14304b99263ac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap @@ -144,11 +144,12 @@ exports[`DataProviders rendering renders correctly against snapshot 1`] = ` }, ] } - id="foo" onDataProviderEdited={[MockFunction]} onDataProviderRemoved={[MockFunction]} onToggleDataProviderEnabled={[MockFunction]} onToggleDataProviderExcluded={[MockFunction]} + onToggleDataProviderType={[MockFunction]} + timelineId="foo" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap index dac95c302af27..006da47460012 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap @@ -20,8 +20,6 @@ exports[`Empty rendering renders correctly against snapshot 1`] = ` highlighted - - + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap index 16094c585911b..d589a9aa33f06 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap @@ -11,6 +11,8 @@ exports[`Provider rendering renders correctly against snapshot 1`] = ` providerId="id-Provider 1" toggleEnabledProvider={[Function]} toggleExcludedProvider={[Function]} + toggleTypeProvider={[Function]} + type="default" val="Provider 1" /> `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap index d0d12a135e3dc..a227f39494b61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap @@ -5,26 +5,24 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - - + - ( - + @@ -42,37 +40,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -90,37 +88,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -138,37 +136,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -186,37 +184,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -234,37 +232,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -282,37 +280,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -330,37 +328,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -378,37 +376,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -426,37 +424,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -474,37 +472,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - + + - + - ( - + @@ -522,13 +520,13 @@ exports[`Providers rendering renders correctly against snapshot 1`] = ` - ) - +
`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx new file mode 100644 index 0000000000000..8e1c02bad50a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { + EuiButton, + EuiContextMenu, + EuiText, + EuiPopover, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import uuid from 'uuid'; +import { useDispatch, useSelector } from 'react-redux'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { TimelineType } from '../../../../../common/types/timeline'; +import { StatefulEditDataProvider } from '../../edit_data_provider'; +import { addContentToTimeline } from './helpers'; +import { DataProviderType } from './data_provider'; +import { timelineSelectors } from '../../../store/timeline'; +import { ADD_FIELD_LABEL, ADD_TEMPLATE_FIELD_LABEL } from './translations'; + +interface AddDataProviderPopoverProps { + browserFields: BrowserFields; + timelineId: string; +} + +const AddDataProviderPopoverComponent: React.FC = ({ + browserFields, + timelineId, +}) => { + const dispatch = useDispatch(); + const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); + const timelineById = useSelector(timelineSelectors.timelineByIdSelector); + const { dataProviders, timelineType } = timelineById[timelineId] ?? {}; + + const handleOpenPopover = useCallback(() => setIsAddFilterPopoverOpen(true), [ + setIsAddFilterPopoverOpen, + ]); + + const handleClosePopover = useCallback(() => setIsAddFilterPopoverOpen(false), [ + setIsAddFilterPopoverOpen, + ]); + + const handleDataProviderEdited = useCallback( + ({ andProviderId, excluded, field, id, operator, providerId, value, type }) => { + addContentToTimeline({ + dataProviders, + destination: { + droppableId: `droppableId.timelineProviders.${timelineId}.group.${dataProviders.length}`, + index: 0, + }, + dispatch, + onAddedToTimeline: handleClosePopover, + providerToAdd: { + id: providerId, + name: value, + enabled: true, + excluded, + kqlQuery: '', + type, + queryMatch: { + displayField: undefined, + displayValue: undefined, + field, + value, + operator, + }, + and: [], + }, + timelineId, + }); + }, + [dataProviders, timelineId, dispatch, handleClosePopover] + ); + + const panels = useMemo( + () => [ + { + id: 0, + width: 400, + items: [ + { + name: ADD_FIELD_LABEL, + icon: , + panel: 1, + }, + timelineType === TimelineType.template + ? { + disabled: timelineType !== TimelineType.template, + name: ADD_TEMPLATE_FIELD_LABEL, + icon: , + panel: 2, + } + : null, + ].filter((item) => item !== null) as EuiContextMenuPanelItemDescriptor[], + }, + { + id: 1, + title: ADD_FIELD_LABEL, + width: 400, + content: ( + + ), + }, + { + id: 2, + title: ADD_TEMPLATE_FIELD_LABEL, + width: 400, + content: ( + + ), + }, + ], + [browserFields, handleDataProviderEdited, timelineId, timelineType] + ); + + const button = useMemo( + () => ( + + {ADD_FIELD_LABEL} + + ), + [handleOpenPopover] + ); + + const content = useMemo(() => { + if (timelineType === TimelineType.template) { + return ; + } + + return ( + + ); + }, [browserFields, handleDataProviderEdited, panels, timelineId, timelineType]); + + return ( + + {content} + + ); +}; + +AddDataProviderPopoverComponent.displayName = 'AddDataProviderPopoverComponent'; + +export const AddDataProviderPopover = React.memo(AddDataProviderPopoverComponent); + +AddDataProviderPopover.displayName = 'AddDataProviderPopover'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts index a6fd8a0ceabbe..7fe0255132bc9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts @@ -15,6 +15,11 @@ export const EXISTS_OPERATOR = ':*'; /** The operator applied to a field */ export type QueryOperator = ':' | ':*'; +export enum DataProviderType { + default = 'default', + template = 'template', +} + export interface QueryMatch { field: string; displayField?: string; @@ -39,7 +44,7 @@ export interface DataProvider { */ excluded: boolean; /** - * Return the KQL query who have been added by user + * Returns the KQL query who have been added by user */ kqlQuery: string; /** @@ -50,6 +55,10 @@ export interface DataProvider { * Additional query clauses that are ANDed with this query to narrow results */ and: DataProvidersAnd[]; + /** + * Returns a DataProviderType + */ + type?: DataProviderType.default | DataProviderType.template; } export type DataProvidersAnd = Pick>; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx index 3a8c0d8831217..754d7f9c47edf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx @@ -37,13 +37,14 @@ describe('DataProviders', () => { @@ -58,12 +59,13 @@ describe('DataProviders', () => { ); @@ -76,12 +78,13 @@ describe('DataProviders', () => { ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx index 598d9233cb01d..e1fad47e4204e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx @@ -13,7 +13,7 @@ import { TestProviders } from '../../../../common/mock/test_providers'; describe('Empty', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); @@ -22,7 +22,7 @@ describe('Empty', () => { test('it renders the expected message', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx index 691c919029261..a6e70791d1ec7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx @@ -8,7 +8,9 @@ import { EuiBadge, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; +import { BrowserFields } from '../../../../common/containers/source'; import { AndOrBadge } from '../../../../common/components/and_or_badge'; +import { AddDataProviderPopover } from './add_data_provider_popover'; import * as i18n from './translations'; @@ -42,7 +44,7 @@ const EmptyContainer = styled.div<{ showSmallMsg: boolean }>` width: ${(props) => (props.showSmallMsg ? '60px' : 'auto')}; align-items: center; display: flex; - flex-direction: row; + flex-direction: column; flex-wrap: wrap; justify-content: center; user-select: none; @@ -72,12 +74,14 @@ const NoWrap = styled.div` NoWrap.displayName = 'NoWrap'; interface Props { + browserFields: BrowserFields; showSmallMsg?: boolean; + timelineId: string; } /** * Prompts the user to drop anything with a facet count into the data providers section. */ -export const Empty = React.memo(({ showSmallMsg = false }) => ( +export const Empty = React.memo(({ showSmallMsg = false, browserFields, timelineId }) => ( (({ showSmallMsg = false }) => ( {i18n.HIGHLIGHTED} - - - {i18n.HERE_TO_BUILD_AN} @@ -105,6 +106,8 @@ export const Empty = React.memo(({ showSmallMsg = false }) => ( {i18n.QUERY} + + )} {showSmallMsg && } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx index 9dc66a930ccc0..923ef86c0bbc0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx @@ -281,6 +281,7 @@ export const addProviderToGroup = ({ } const destinationGroupIndex = getGroupIndexFromDroppableId(destination.droppableId); + if ( indexIsValid({ index: destinationGroupIndex, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 90411f975da0b..c9e06f89af41c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -19,6 +19,7 @@ import { OnDataProviderRemoved, OnToggleDataProviderEnabled, OnToggleDataProviderExcluded, + OnToggleDataProviderType, } from '../events'; import { DataProvider } from './data_provider'; @@ -28,12 +29,13 @@ import { useManageTimeline } from '../../manage_timeline'; interface Props { browserFields: BrowserFields; - id: string; + timelineId: string; dataProviders: DataProvider[]; onDataProviderEdited: OnDataProviderEdited; onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + onToggleDataProviderType: OnToggleDataProviderType; } const DropTargetDataProvidersContainer = styled.div` @@ -61,6 +63,7 @@ const DropTargetDataProviders = styled.div` position: relative; border: 0.2rem dashed ${(props) => props.theme.eui.euiColorMediumShade}; border-radius: 5px; + padding: 5px 0; margin: 2px 0 2px 0; min-height: 100px; overflow-y: auto; @@ -91,17 +94,18 @@ const getDroppableId = (id: string): string => `${droppableTimelineProvidersPref export const DataProviders = React.memo( ({ browserFields, - id, dataProviders, + timelineId, onDataProviderEdited, onDataProviderRemoved, onToggleDataProviderEnabled, onToggleDataProviderExcluded, + onToggleDataProviderType, }) => { const { getManageTimelineById } = useManageTimeline(); - const isLoading = useMemo(() => getManageTimelineById(id).isLoading, [ + const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [ getManageTimelineById, - id, + timelineId, ]); return ( @@ -112,16 +116,17 @@ export const DataProviders = React.memo( {dataProviders != null && dataProviders.length ? ( ) : ( - - + + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx index 8fd164eb8a3e2..2b598c7cf04f0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx @@ -7,7 +7,7 @@ import { noop } from 'lodash/fp'; import React from 'react'; -import { DataProvider, IS_OPERATOR } from './data_provider'; +import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider'; import { ProviderItemBadge } from './provider_item_badge'; interface OwnProps { @@ -24,8 +24,10 @@ export const Provider = React.memo(({ dataProvider }) => ( providerId={dataProvider.id} toggleExcludedProvider={noop} toggleEnabledProvider={noop} + toggleTypeProvider={noop} val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value} operator={dataProvider.queryMatch.operator || IS_OPERATOR} + type={dataProvider.type || DataProviderType.default} /> )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx index b3682c0d55147..af63957d35075 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx @@ -10,14 +10,20 @@ import { isString } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { TimelineType } from '../../../../../common/types/timeline'; import { getEmptyString } from '../../../../common/components/empty_value'; import { ProviderContainer } from '../../../../common/components/drag_and_drop/provider_container'; -import { EXISTS_OPERATOR, QueryOperator } from './data_provider'; +import { DataProviderType, EXISTS_OPERATOR, QueryOperator } from './data_provider'; import * as i18n from './translations'; -const ProviderBadgeStyled = (styled(EuiBadge)` +type ProviderBadgeStyledType = typeof EuiBadge & { + // https://styled-components.com/docs/api#transient-props + $timelineType: TimelineType; +}; + +const ProviderBadgeStyled = styled(EuiBadge)` .euiToolTipAnchor { &::after { font-style: normal; @@ -25,17 +31,29 @@ const ProviderBadgeStyled = (styled(EuiBadge)` padding: 0px 3px; } } + &.globalFilterItem { white-space: nowrap; + min-width: ${({ $timelineType }) => + $timelineType === TimelineType.template ? '140px' : 'none'}; + display: flex; + &.globalFilterItem-isDisabled { text-decoration: line-through; font-weight: 400; font-style: italic; } + + &.globalFilterItem-isError { + box-shadow: 0 1px 1px -1px rgba(152, 162, 179, 0.2), 0 3px 2px -2px rgba(152, 162, 179, 0.2), + inset 0 0 0 1px #bd271e; + } } + .euiBadge.euiBadge--iconLeft &.euiBadge.euiBadge--iconRight .euiBadge__content { flex-direction: row; } + .euiBadge.euiBadge--iconLeft &.euiBadge.euiBadge--iconRight .euiBadge__content @@ -43,10 +61,46 @@ const ProviderBadgeStyled = (styled(EuiBadge)` margin-right: 0; margin-left: 4px; } -` as unknown) as typeof EuiBadge; +`; ProviderBadgeStyled.displayName = 'ProviderBadgeStyled'; +const ProviderFieldBadge = styled.div` + display: block; + color: #fff; + padding: 6px 8px; + font-size: 0.6em; +`; + +const StyledTemplateFieldBadge = styled(ProviderFieldBadge)` + background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; + text-transform: uppercase; +`; + +interface TemplateFieldBadgeProps { + type: DataProviderType; + toggleType: () => void; +} + +const ConvertFieldBadge = styled(ProviderFieldBadge)` + background: ${({ theme }) => theme.eui.euiColorDarkShade}; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +`; + +const TemplateFieldBadge: React.FC = ({ type, toggleType }) => { + if (type === DataProviderType.default) { + return ( + {i18n.CONVERT_TO_TEMPLATE_FIELD} + ); + } + + return {i18n.TEMPLATE_FIELD_LABEL}; +}; + interface ProviderBadgeProps { deleteProvider: () => void; field: string; @@ -55,8 +109,11 @@ interface ProviderBadgeProps { isExcluded: boolean; providerId: string; togglePopover: () => void; + toggleType: () => void; val: string | number; operator: QueryOperator; + type: DataProviderType; + timelineType: TimelineType; } const closeButtonProps = { @@ -66,7 +123,19 @@ const closeButtonProps = { }; export const ProviderBadge = React.memo( - ({ deleteProvider, field, isEnabled, isExcluded, operator, providerId, togglePopover, val }) => { + ({ + deleteProvider, + field, + isEnabled, + isExcluded, + operator, + providerId, + togglePopover, + toggleType, + val, + type, + timelineType, + }) => { const deleteFilter: React.MouseEventHandler = useCallback( (event: React.MouseEvent) => { // Make sure it doesn't also trigger the onclick for the whole badge @@ -93,34 +162,46 @@ export const ProviderBadge = React.memo( const prefix = useMemo(() => (isExcluded ? {i18n.NOT} : null), [isExcluded]); - return ( - - + const content = useMemo( + () => ( + <> {prefix} {operator !== EXISTS_OPERATOR ? ( - <> - {`${field}: `} - {`"${formattedValue}"`} - + {`${field}: "${formattedValue}"`} ) : ( {field} {i18n.EXISTS_LABEL} )} - + + ), + [field, formattedValue, operator, prefix] + ); + + return ( + + <> + + {content} + + + {timelineType === TimelineType.template && ( + + )} + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx index 540b1b80259a0..7aa782c05c0dd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx @@ -12,9 +12,11 @@ import { import React, { FunctionComponent } from 'react'; import styled from 'styled-components'; +import { TimelineType } from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; + import { OnDataProviderEdited } from '../events'; -import { QueryOperator, EXISTS_OPERATOR } from './data_provider'; +import { DataProviderType, QueryOperator, EXISTS_OPERATOR } from './data_provider'; import { StatefulEditDataProvider } from '../../edit_data_provider'; import * as i18n from './translations'; @@ -23,6 +25,7 @@ export const EDIT_CLASS_NAME = 'edit-data-provider'; export const EXCLUDE_CLASS_NAME = 'exclude-data-provider'; export const ENABLE_CLASS_NAME = 'enable-data-provider'; export const FILTER_FOR_FIELD_PRESENT_CLASS_NAME = 'filter-for-field-present-data-provider'; +export const CONVERT_TO_FIELD_CLASS_NAME = 'convert-to-field-data-provider'; export const DELETE_CLASS_NAME = 'delete-data-provider'; interface OwnProps { @@ -41,9 +44,12 @@ interface OwnProps { operator: QueryOperator; providerId: string; timelineId?: string; + timelineType?: TimelineType; toggleEnabledProvider: () => void; toggleExcludedProvider: () => void; + toggleTypeProvider: () => void; value: string | number; + type: DataProviderType; } const MyEuiPopover = styled((EuiPopover as unknown) as FunctionComponent)< @@ -57,6 +63,27 @@ const MyEuiPopover = styled((EuiPopover as unknown) as FunctionComponent)< MyEuiPopover.displayName = 'MyEuiPopover'; +interface GetProviderActionsProps { + andProviderId?: string; + browserFields?: BrowserFields; + deleteItem: () => void; + field: string; + isEnabled: boolean; + isExcluded: boolean; + isLoading: boolean; + onDataProviderEdited?: OnDataProviderEdited; + onFilterForFieldPresent: () => void; + operator: QueryOperator; + providerId: string; + timelineId?: string; + timelineType?: TimelineType; + toggleEnabled: () => void; + toggleExcluded: () => void; + toggleType: () => void; + value: string | number; + type: DataProviderType; +} + export const getProviderActions = ({ andProviderId, browserFields, @@ -70,26 +97,13 @@ export const getProviderActions = ({ onFilterForFieldPresent, providerId, timelineId, + timelineType, toggleEnabled, toggleExcluded, + toggleType, + type, value, -}: { - andProviderId?: string; - browserFields?: BrowserFields; - deleteItem: () => void; - field: string; - isEnabled: boolean; - isExcluded: boolean; - isLoading: boolean; - onDataProviderEdited?: OnDataProviderEdited; - onFilterForFieldPresent: () => void; - operator: QueryOperator; - providerId: string; - timelineId?: string; - toggleEnabled: () => void; - toggleExcluded: () => void; - value: string | number; -}): EuiContextMenuPanelDescriptor[] => [ +}: GetProviderActionsProps): EuiContextMenuPanelDescriptor[] => [ { id: 0, items: [ @@ -121,6 +135,18 @@ export const getProviderActions = ({ name: i18n.FILTER_FOR_FIELD_PRESENT, onClick: onFilterForFieldPresent, }, + timelineType === TimelineType.template + ? { + className: CONVERT_TO_FIELD_CLASS_NAME, + disabled: isLoading, + icon: 'visText', + name: + type === DataProviderType.template + ? i18n.CONVERT_TO_FIELD + : i18n.CONVERT_TO_TEMPLATE_FIELD, + onClick: toggleType, + } + : { name: null }, { className: DELETE_CLASS_NAME, disabled: isLoading, @@ -128,7 +154,7 @@ export const getProviderActions = ({ name: i18n.DELETE_DATA_PROVIDER, onClick: deleteItem, }, - ], + ].filter((item) => item.name != null), }, { content: @@ -143,6 +169,7 @@ export const getProviderActions = ({ providerId={providerId} timelineId={timelineId} value={value} + type={type} /> ) : null, id: 1, @@ -167,9 +194,12 @@ export class ProviderItemActions extends React.PureComponent { operator, providerId, timelineId, + timelineType, toggleEnabledProvider, toggleExcludedProvider, + toggleTypeProvider, value, + type, } = this.props; const panelTree = getProviderActions({ @@ -185,9 +215,12 @@ export class ProviderItemActions extends React.PureComponent { operator, providerId, timelineId, + timelineType, toggleEnabled: toggleEnabledProvider, toggleExcluded: toggleExcludedProvider, + toggleType: toggleTypeProvider, value, + type, }); return ( @@ -214,6 +247,7 @@ export class ProviderItemActions extends React.PureComponent { operator, providerId, value, + type, }) => { if (this.props.onDataProviderEdited != null) { this.props.onDataProviderEdited({ @@ -224,6 +258,7 @@ export class ProviderItemActions extends React.PureComponent { operator, providerId, value, + type, }); } @@ -231,7 +266,7 @@ export class ProviderItemActions extends React.PureComponent { }; private onFilterForFieldPresent = () => { - const { andProviderId, field, timelineId, providerId, value } = this.props; + const { andProviderId, field, timelineId, providerId, value, type } = this.props; if (this.props.onDataProviderEdited != null) { this.props.onDataProviderEdited({ @@ -242,6 +277,7 @@ export class ProviderItemActions extends React.PureComponent { operator: EXISTS_OPERATOR, providerId, value, + type, }); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index 1f6fe998a44e9..bc7c313553f1e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -6,14 +6,16 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { TimelineType } from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; +import { timelineSelectors } from '../../../store/timeline'; import { OnDataProviderEdited } from '../events'; import { ProviderBadge } from './provider_badge'; import { ProviderItemActions } from './provider_item_actions'; -import { DataProvidersAnd, QueryOperator } from './data_provider'; +import { DataProvidersAnd, DataProviderType, QueryOperator } from './data_provider'; import { dragAndDropActions } from '../../../../common/store/drag_and_drop'; import { useManageTimeline } from '../../manage_timeline'; @@ -32,7 +34,9 @@ interface ProviderItemBadgeProps { timelineId?: string; toggleEnabledProvider: () => void; toggleExcludedProvider: () => void; + toggleTypeProvider: () => void; val: string | number; + type?: DataProviderType; } export const ProviderItemBadge = React.memo( @@ -51,8 +55,12 @@ export const ProviderItemBadge = React.memo( timelineId, toggleEnabledProvider, toggleExcludedProvider, + toggleTypeProvider, val, + type = DataProviderType.default, }) => { + const timelineById = useSelector(timelineSelectors.timelineByIdSelector); + const timelineType = timelineId ? timelineById[timelineId]?.timelineType : TimelineType.default; const { getManageTimelineById } = useManageTimeline(); const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [ getManageTimelineById, @@ -71,14 +79,17 @@ export const ProviderItemBadge = React.memo( const onToggleEnabledProvider = useCallback(() => { toggleEnabledProvider(); closePopover(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toggleEnabledProvider]); + }, [closePopover, toggleEnabledProvider]); const onToggleExcludedProvider = useCallback(() => { toggleExcludedProvider(); closePopover(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toggleExcludedProvider]); + }, [toggleExcludedProvider, closePopover]); + + const onToggleTypeProvider = useCallback(() => { + toggleTypeProvider(); + closePopover(); + }, [toggleTypeProvider, closePopover]); const [providerRegistered, setProviderRegistered] = useState(false); @@ -102,27 +113,31 @@ export const ProviderItemBadge = React.memo( () => () => { unRegisterProvider(); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [unRegisterProvider] + ); + + const button = ( + ); return ( - } + button={button} closePopover={closePopover} deleteProvider={deleteProvider} field={field} @@ -135,9 +150,12 @@ export const ProviderItemBadge = React.memo( operator={operator} providerId={providerId} timelineId={timelineId} + timelineType={timelineType} toggleEnabledProvider={onToggleEnabledProvider} toggleExcludedProvider={onToggleExcludedProvider} + toggleTypeProvider={onToggleTypeProvider} value={val} + type={type} /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx index 9dc0b76224458..b788f70cb2e4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx @@ -38,11 +38,12 @@ describe('Providers', () => { ); expect(wrapper).toMatchSnapshot(); @@ -55,11 +56,12 @@ describe('Providers', () => { @@ -82,11 +84,12 @@ describe('Providers', () => { @@ -107,11 +110,12 @@ describe('Providers', () => { @@ -134,11 +138,12 @@ describe('Providers', () => { @@ -163,11 +168,12 @@ describe('Providers', () => { @@ -195,11 +201,12 @@ describe('Providers', () => { @@ -227,11 +234,12 @@ describe('Providers', () => { @@ -260,11 +268,12 @@ describe('Providers', () => { @@ -295,11 +304,12 @@ describe('Providers', () => { @@ -330,11 +340,12 @@ describe('Providers', () => { @@ -344,9 +355,9 @@ describe('Providers', () => { '[data-test-subj="providerBadge"] .euiBadge__content span.field-value' ); const andProviderBadgesText = andProviderBadges.map((node) => node.text()).join(' '); - expect(andProviderBadges.length).toEqual(6); + expect(andProviderBadges.length).toEqual(3); expect(andProviderBadgesText).toEqual( - 'name: "Provider 1" name: "Provider 2" name: "Provider 3"' + 'name: "Provider 1" name: "Provider 2" name: "Provider 3"' ); }); @@ -361,11 +372,12 @@ describe('Providers', () => { @@ -395,11 +407,12 @@ describe('Providers', () => { @@ -429,11 +442,12 @@ describe('Providers', () => { @@ -472,11 +486,12 @@ describe('Providers', () => { @@ -511,11 +526,12 @@ describe('Providers', () => { @@ -554,11 +570,12 @@ describe('Providers', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx index b5d44cf854458..c9dd906cee59b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiFormHelpText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormHelpText, EuiSpacer } from '@elastic/eui'; import { rgba } from 'polished'; -import React, { useMemo } from 'react'; +import React, { Fragment, useMemo } from 'react'; import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd'; import styled, { css } from 'styled-components'; import { AndOrBadge } from '../../../../common/components/and_or_badge'; +import { AddDataProviderPopover } from './add_data_provider_popover'; import { BrowserFields } from '../../../../common/containers/source'; import { getTimelineProviderDroppableId, @@ -22,9 +23,10 @@ import { OnDataProviderRemoved, OnToggleDataProviderEnabled, OnToggleDataProviderExcluded, + OnToggleDataProviderType, } from '../events'; -import { DataProvider, DataProvidersAnd, IS_OPERATOR } from './data_provider'; +import { DataProvider, DataProviderType, DataProvidersAnd, IS_OPERATOR } from './data_provider'; import { EMPTY_GROUP, flattenIntoAndGroups } from './helpers'; import { ProviderItemBadge } from './provider_item_badge'; @@ -32,12 +34,13 @@ export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group'; interface Props { browserFields: BrowserFields; - id: string; + timelineId: string; dataProviders: DataProvider[]; onDataProviderEdited: OnDataProviderEdited; onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + onToggleDataProviderType: OnToggleDataProviderType; } /** @@ -62,7 +65,8 @@ const getItemStyle = ( }); const DroppableContainer = styled.div` - height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px; + min-height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px; + height: auto !important; .${IS_DRAGGING_CLASS_NAME} &:hover { background-color: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important; @@ -78,10 +82,10 @@ const Parens = styled.span` `} `; -const AndOrBadgeContainer = styled.div<{ hideBadge: boolean }>` - span { - visibility: ${({ hideBadge }) => (hideBadge ? 'hidden' : 'inherit')}; - } +const AndOrBadgeContainer = styled.div` + width: 121px; + display: flex; + justify-content: flex-end; `; const LastAndOrBadgeInGroup = styled.div` @@ -105,6 +109,17 @@ const TimelineEuiFormHelpText = styled(EuiFormHelpText)` TimelineEuiFormHelpText.displayName = 'TimelineEuiFormHelpText'; +const ParensContainer = styled(EuiFlexItem)` + align-self: center; +`; + +const AddDataProviderContainer = styled.div` + padding-right: 9px; +`; + +const getDataProviderValue = (dataProvider: DataProvidersAnd) => + dataProvider.queryMatch.displayValue ?? dataProvider.queryMatch.value; + /** * Renders an interactive card representation of the data providers. It also * affords uniform UI controls for the following actions: @@ -115,148 +130,178 @@ TimelineEuiFormHelpText.displayName = 'TimelineEuiFormHelpText'; export const Providers = React.memo( ({ browserFields, - id, + timelineId, dataProviders, onDataProviderEdited, onDataProviderRemoved, onToggleDataProviderEnabled, onToggleDataProviderExcluded, + onToggleDataProviderType, }) => { // Transform the dataProviders into flattened groups, and append an empty group const dataProviderGroups: DataProvidersAnd[][] = useMemo( () => [...flattenIntoAndGroups(dataProviders), ...EMPTY_GROUP], [dataProviders] ); + return (
{dataProviderGroups.map((group, groupIndex) => ( - - - - - - - - {'('} - - - - {(droppableProvided) => ( - - {group.map((dataProvider, index) => ( - - {(provided, snapshot) => ( -
- - - 0 ? dataProvider.id : undefined} - browserFields={browserFields} - deleteProvider={() => - index > 0 - ? onDataProviderRemoved(group[0].id, dataProvider.id) - : onDataProviderRemoved(dataProvider.id) - } - field={ - index > 0 - ? dataProvider.queryMatch.displayField ?? - dataProvider.queryMatch.field - : group[0].queryMatch.displayField ?? - group[0].queryMatch.field - } - kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery} - isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled} - isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded} - onDataProviderEdited={onDataProviderEdited} - operator={ - index > 0 - ? dataProvider.queryMatch.operator ?? IS_OPERATOR - : group[0].queryMatch.operator ?? IS_OPERATOR - } - register={dataProvider} - providerId={index > 0 ? group[0].id : dataProvider.id} - timelineId={id} - toggleEnabledProvider={() => - index > 0 - ? onToggleDataProviderEnabled({ - providerId: group[0].id, - enabled: !dataProvider.enabled, - andProviderId: dataProvider.id, - }) - : onToggleDataProviderEnabled({ - providerId: dataProvider.id, - enabled: !dataProvider.enabled, - }) - } - toggleExcludedProvider={() => - index > 0 - ? onToggleDataProviderExcluded({ - providerId: group[0].id, - excluded: !dataProvider.excluded, - andProviderId: dataProvider.id, - }) - : onToggleDataProviderExcluded({ - providerId: dataProvider.id, - excluded: !dataProvider.excluded, - }) - } - val={ - dataProvider.queryMatch.displayValue ?? - dataProvider.queryMatch.value - } - /> - - - {!snapshot.isDragging && - (index < group.length - 1 ? ( - - ) : ( - - - - ))} - - -
- )} -
- ))} - {droppableProvided.placeholder} -
+ + {groupIndex !== 0 && } + + + + {groupIndex === 0 ? ( + + + + ) : ( + + + )} -
-
- - {')'} - -
+ + + {'('} + + + + {(droppableProvided) => ( + + {group.map((dataProvider, index) => ( + + {(provided, snapshot) => ( +
+ + + 0 ? dataProvider.id : undefined} + browserFields={browserFields} + deleteProvider={() => + index > 0 + ? onDataProviderRemoved(group[0].id, dataProvider.id) + : onDataProviderRemoved(dataProvider.id) + } + field={ + index > 0 + ? dataProvider.queryMatch.displayField ?? + dataProvider.queryMatch.field + : group[0].queryMatch.displayField ?? + group[0].queryMatch.field + } + kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery} + isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled} + isExcluded={ + index > 0 ? dataProvider.excluded : group[0].excluded + } + onDataProviderEdited={onDataProviderEdited} + operator={ + index > 0 + ? dataProvider.queryMatch.operator ?? IS_OPERATOR + : group[0].queryMatch.operator ?? IS_OPERATOR + } + register={dataProvider} + providerId={index > 0 ? group[0].id : dataProvider.id} + timelineId={timelineId} + toggleEnabledProvider={() => + index > 0 + ? onToggleDataProviderEnabled({ + providerId: group[0].id, + enabled: !dataProvider.enabled, + andProviderId: dataProvider.id, + }) + : onToggleDataProviderEnabled({ + providerId: dataProvider.id, + enabled: !dataProvider.enabled, + }) + } + toggleExcludedProvider={() => + index > 0 + ? onToggleDataProviderExcluded({ + providerId: group[0].id, + excluded: !dataProvider.excluded, + andProviderId: dataProvider.id, + }) + : onToggleDataProviderExcluded({ + providerId: dataProvider.id, + excluded: !dataProvider.excluded, + }) + } + toggleTypeProvider={() => + index > 0 + ? onToggleDataProviderType({ + providerId: group[0].id, + type: + dataProvider.type === DataProviderType.template + ? DataProviderType.default + : DataProviderType.template, + andProviderId: dataProvider.id, + }) + : onToggleDataProviderType({ + providerId: dataProvider.id, + type: + dataProvider.type === DataProviderType.template + ? DataProviderType.default + : DataProviderType.template, + }) + } + val={getDataProviderValue(dataProvider)} + type={dataProvider.type} + /> + + + {!snapshot.isDragging && + (index < group.length - 1 ? ( + + ) : ( + + + + ))} + + +
+ )} +
+ ))} + {droppableProvided.placeholder} +
+ )} +
+
+ + {')'} + + + ))}
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts index 104ff44cb9b7c..48f1f4e2218d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts @@ -72,6 +72,20 @@ export const FILTER_FOR_FIELD_PRESENT = i18n.translate( } ); +export const CONVERT_TO_FIELD = i18n.translate( + 'xpack.securitySolution.dataProviders.convertToFieldLabel', + { + defaultMessage: 'Convert to field', + } +); + +export const CONVERT_TO_TEMPLATE_FIELD = i18n.translate( + 'xpack.securitySolution.dataProviders.convertToTemplateFieldLabel', + { + defaultMessage: 'Convert to template field', + } +); + export const HIGHLIGHTED = i18n.translate('xpack.securitySolution.dataProviders.highlighted', { defaultMessage: 'highlighted', }); @@ -148,3 +162,24 @@ export const VALUE_ARIA_LABEL = i18n.translate( defaultMessage: 'value', } ); + +export const ADD_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.dataProviders.addFieldPopoverButtonLabel', + { + defaultMessage: 'Add field', + } +); + +export const ADD_TEMPLATE_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.dataProviders.addTemplateFieldPopoverButtonLabel', + { + defaultMessage: 'Add template field', + } +); + +export const TEMPLATE_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.dataProviders.templateFieldLabel', + { + defaultMessage: 'Template field', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts index 6c9a9b8b89679..4653880739c6d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts @@ -7,7 +7,7 @@ import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { ColumnId } from './body/column_id'; import { SortDirection } from './body/sort'; -import { QueryOperator } from './data_providers/data_provider'; +import { DataProvider, DataProviderType, QueryOperator } from './data_providers/data_provider'; /** Invoked when a user clicks the close button to remove a data provider */ export type OnDataProviderRemoved = (providerId: string, andProviderId?: string) => void; @@ -26,6 +26,13 @@ export type OnToggleDataProviderExcluded = (excluded: { andProviderId?: string; }) => void; +/** Invoked when a user toggles type (can "default" or "template") of a data provider */ +export type OnToggleDataProviderType = (type: { + providerId: string; + type: DataProviderType; + andProviderId?: string; +}) => void; + /** Invoked when a user edits the properties of a data provider */ export type OnDataProviderEdited = ({ andProviderId, @@ -35,6 +42,7 @@ export type OnDataProviderEdited = ({ operator, providerId, value, + type, }: { andProviderId?: string; excluded: boolean; @@ -43,6 +51,7 @@ export type OnDataProviderEdited = ({ operator: QueryOperator; providerId: string; value: string | number; + type: DataProvider['type']; }) => void; /** Invoked when a user change the kql query of our data provider */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap index b3b39236150ec..f94c30c5a102d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -138,11 +138,12 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` }, ] } - id="foo" onDataProviderEdited={[MockFunction]} onDataProviderRemoved={[MockFunction]} onToggleDataProviderEnabled={[MockFunction]} onToggleDataProviderExcluded={[MockFunction]} + onToggleDataProviderType={[MockFunction]} + timelineId="foo" /> { browserFields: {}, dataProviders: mockDataProviders, filterManager: new FilterManager(mockUiSettingsForFilterManager), - id: 'foo', indexPattern, onDataProviderEdited: jest.fn(), onDataProviderRemoved: jest.fn(), onToggleDataProviderEnabled: jest.fn(), onToggleDataProviderExcluded: jest.fn(), + onToggleDataProviderType: jest.fn(), show: true, showCallOutUnauthorizedMsg: false, status: TimelineStatus.active, + timelineId: 'foo', + timelineType: TimelineType.default, }; describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index 0541dee4b1e52..93af374b15b56 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -17,6 +17,7 @@ import { OnDataProviderRemoved, OnToggleDataProviderEnabled, OnToggleDataProviderExcluded, + OnToggleDataProviderType, } from '../events'; import { StatefulSearchOrFilter } from '../search_or_filter'; import { BrowserFields } from '../../../../common/containers/source'; @@ -32,20 +33,20 @@ interface Props { dataProviders: DataProvider[]; filterManager: FilterManager; graphEventId?: string; - id: string; indexPattern: IIndexPattern; onDataProviderEdited: OnDataProviderEdited; onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + onToggleDataProviderType: OnToggleDataProviderType; show: boolean; showCallOutUnauthorizedMsg: boolean; status: TimelineStatusLiteralWithNull; + timelineId: string; } const TimelineHeaderComponent: React.FC = ({ browserFields, - id, indexPattern, dataProviders, filterManager, @@ -54,9 +55,11 @@ const TimelineHeaderComponent: React.FC = ({ onDataProviderRemoved, onToggleDataProviderEnabled, onToggleDataProviderExcluded, + onToggleDataProviderType, show, showCallOutUnauthorizedMsg, status, + timelineId, }) => ( <> {showCallOutUnauthorizedMsg && ( @@ -81,19 +84,20 @@ const TimelineHeaderComponent: React.FC = ({ <> )} @@ -104,7 +108,6 @@ export const TimelineHeader = React.memo( TimelineHeaderComponent, (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && - prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && prevProps.filterManager === nextProps.filterManager && @@ -113,7 +116,9 @@ export const TimelineHeader = React.memo( prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved && prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled && prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded && + prevProps.onToggleDataProviderType === nextProps.onToggleDataProviderType && prevProps.show === nextProps.show && prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && - prevProps.status === nextProps.status + prevProps.status === nextProps.status && + prevProps.timelineId === nextProps.timelineId ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx index 1038ac4b69587..391d367ad3dc3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx @@ -7,6 +7,7 @@ import { cloneDeep } from 'lodash/fp'; import { mockIndexPattern } from '../../../common/mock'; +import { DataProviderType } from './data_providers/data_provider'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { buildGlobalQuery, combineQueries } from './helpers'; import { mockBrowserFields } from '../../../common/containers/source/mock'; @@ -23,6 +24,20 @@ describe('Build KQL Query', () => { expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); }); + test('Build KQL query with one template data provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].type = DataProviderType.template; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name :*'); + }); + + test('Build KQL query with one disabled data provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].enabled = false; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual(''); + }); + test('Build KQL query with one data provider as timestamp (string input)', () => { const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); dataProviders[0].queryMatch.field = '@timestamp'; @@ -75,6 +90,20 @@ describe('Build KQL Query', () => { expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); }); + test('Build KQL query with two data provider (first is template)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].type = DataProviderType.template; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name :*) or (name : "Provider 2")'); + }); + + test('Build KQL query with two data provider (second is template)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[1].type = DataProviderType.template; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name :*)'); + }); + test('Build KQL query with one data provider and one and', () => { const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index a3fc692c3a8a8..a0087ab638dbf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -9,7 +9,12 @@ import memoizeOne from 'memoize-one'; import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury'; -import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; +import { + DataProvider, + DataProviderType, + DataProvidersAnd, + EXISTS_OPERATOR, +} from './data_providers/data_provider'; import { BrowserFields } from '../../../common/containers/source'; import { IIndexPattern, @@ -52,7 +57,8 @@ const buildQueryMatch = ( browserFields: BrowserFields ) => `${dataProvider.excluded ? 'NOT ' : ''}${ - dataProvider.queryMatch.operator !== EXISTS_OPERATOR + dataProvider.queryMatch.operator !== EXISTS_OPERATOR && + dataProvider.type !== DataProviderType.template ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) : `${dataProvider.queryMatch.field} : ${ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 296b24cff43ad..50a7782012b76 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -13,7 +13,7 @@ import useResizeObserver from 'use-resize-observer/polyfilled'; import { useSignalIndex, ReturnSignalIndex, -} from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; +} from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { mocksSource } from '../../../common/containers/source/mock'; import { wait } from '../../../common/lib/helpers'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; @@ -40,7 +40,7 @@ jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); const mockUseSignalIndex: jest.Mock = useSignalIndex as jest.Mock; -jest.mock('../../../alerts/containers/detection_engine/alerts/use_signal_index'); +jest.mock('../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -76,6 +76,7 @@ describe('StatefulTimeline', () => { graphEventId: undefined, id: 'foo', isLive: false, + isSaving: false, isTimelineExists: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], @@ -95,6 +96,7 @@ describe('StatefulTimeline', () => { updateDataProviderEnabled: timelineActions.updateDataProviderEnabled, updateDataProviderExcluded: timelineActions.updateDataProviderExcluded, updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery, + updateDataProviderType: timelineActions.updateDataProviderType, updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, updateItemsPerPage: timelineActions.updateItemsPerPage, updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 35622eddc359c..5265efc8109a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { NO_ALERT_INDEX } from '../../../../common/constants'; import { useWithSource } from '../../../common/containers/source'; -import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; +import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; @@ -22,6 +23,7 @@ import { OnDataProviderEdited, OnToggleDataProviderEnabled, OnToggleDataProviderExcluded, + OnToggleDataProviderType, } from './events'; import { Timeline } from './timeline'; @@ -44,6 +46,7 @@ const StatefulTimelineComponent = React.memo( graphEventId, id, isLive, + isSaving, isTimelineExists, itemsPerPage, itemsPerPageOptions, @@ -61,6 +64,7 @@ const StatefulTimelineComponent = React.memo( timelineType, updateDataProviderEnabled, updateDataProviderExcluded, + updateDataProviderType, updateItemsPerPage, upsertColumn, usersViewing, @@ -82,8 +86,7 @@ const StatefulTimelineComponent = React.memo( const onDataProviderRemoved: OnDataProviderRemoved = useCallback( (providerId: string, andProviderId?: string) => removeProvider!({ id, providerId, andProviderId }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, removeProvider] ); const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = useCallback( @@ -94,8 +97,7 @@ const StatefulTimelineComponent = React.memo( providerId, andProviderId, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateDataProviderEnabled] ); const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = useCallback( @@ -106,8 +108,18 @@ const StatefulTimelineComponent = React.memo( providerId, andProviderId, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateDataProviderExcluded] + ); + + const onToggleDataProviderType: OnToggleDataProviderType = useCallback( + ({ providerId, type, andProviderId }) => + updateDataProviderType!({ + id, + type, + providerId, + andProviderId, + }), + [id, updateDataProviderType] ); const onDataProviderEditedLocal: OnDataProviderEdited = useCallback( @@ -121,14 +133,12 @@ const StatefulTimelineComponent = React.memo( providerId, value, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, onDataProviderEdited] ); const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( (itemsChangedPerPage) => updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateItemsPerPage] ); const toggleColumn = useCallback( @@ -176,6 +186,7 @@ const StatefulTimelineComponent = React.memo( indexPattern={indexPattern} indexToAdd={indexToAdd} isLive={isLive} + isSaving={isSaving} itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} kqlMode={kqlMode} @@ -187,12 +198,14 @@ const StatefulTimelineComponent = React.memo( onDataProviderRemoved={onDataProviderRemoved} onToggleDataProviderEnabled={onToggleDataProviderEnabled} onToggleDataProviderExcluded={onToggleDataProviderExcluded} + onToggleDataProviderType={onToggleDataProviderType} show={show!} showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg} sort={sort!} start={start} status={status} toggleColumn={toggleColumn} + timelineType={timelineType} usersViewing={usersViewing} /> ); @@ -204,6 +217,7 @@ const StatefulTimelineComponent = React.memo( prevProps.graphEventId === nextProps.graphEventId && prevProps.id === nextProps.id && prevProps.isLive === nextProps.isLive && + prevProps.isSaving === nextProps.isSaving && prevProps.itemsPerPage === nextProps.itemsPerPage && prevProps.kqlMode === nextProps.kqlMode && prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && @@ -240,15 +254,19 @@ const makeMapStateToProps = () => { graphEventId, itemsPerPage, itemsPerPageOptions, + isSaving, kqlMode, show, sort, status, timelineType, } = timeline; - const kqlQueryExpression = getKqlQueryTimeline(state, id)!; - + const kqlQueryTimeline = getKqlQueryTimeline(state, id)!; const timelineFilter = kqlMode === 'filter' ? filters || [] : []; + + // return events on empty search + const kqlQueryExpression = + isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; return { columns, dataProviders, @@ -258,6 +276,7 @@ const makeMapStateToProps = () => { graphEventId, id, isLive: input.policy.kind === 'interval', + isSaving, isTimelineExists: getTimeline(state, id) != null, itemsPerPage, itemsPerPageOptions, @@ -284,6 +303,7 @@ const mapDispatchToProps = { updateDataProviderEnabled: timelineActions.updateDataProviderEnabled, updateDataProviderExcluded: timelineActions.updateDataProviderExcluded, updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery, + updateDataProviderType: timelineActions.updateDataProviderType, updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, updateItemsPerPage: timelineActions.updateItemsPerPage, updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 7b5e9c0c4c949..452808e51c096 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -119,22 +119,32 @@ Description.displayName = 'Description'; interface NameProps { timelineId: string; + timelineType: TimelineType; title: string; updateTitle: UpdateTitle; } -export const Name = React.memo(({ timelineId, title, updateTitle }) => ( - - updateTitle({ id: timelineId, title: e.target.value })} - placeholder={i18n.UNTITLED_TIMELINE} - spellCheck={true} - value={title} - /> - -)); +export const Name = React.memo(({ timelineId, timelineType, title, updateTitle }) => { + const handleChange = useCallback((e) => updateTitle({ id: timelineId, title: e.target.value }), [ + timelineId, + updateTitle, + ]); + + return ( + + + + ); +}); Name.displayName = 'Name'; interface NewCaseProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 3a28c26a16c9a..ce99304c676ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -6,6 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; + import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { mockGlobalState, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index b3567151c74b3..6de40725f461c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -27,15 +27,6 @@ import { useKibana } from '../../../../common/lib/kibana'; import { APP_ID } from '../../../../../common/constants'; import { getCaseDetailsUrl } from '../../../../common/components/link_to'; -type CreateTimeline = ({ - id, - show, - timelineType, -}: { - id: string; - show?: boolean; - timelineType?: TimelineTypeLiteral; -}) => void; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; @@ -43,7 +34,6 @@ type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; interface Props { associateNote: AssociateNote; - createTimeline: CreateTimeline; description: string; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; @@ -78,7 +68,6 @@ const settingsWidth = 55; export const Properties = React.memo( ({ associateNote, - createTimeline, description, getNotesByIds, graphEventId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx index 4673ba662b2e9..a3cd8802c36bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx @@ -13,7 +13,6 @@ import { AssociateNote, UpdateNote } from '../../notes/helpers'; import { Note } from '../../../../common/lib/note'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; - import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline'; import * as i18n from './translations'; @@ -106,7 +105,12 @@ export const PropertiesLeft = React.memo( /> - + {showDescription ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx index a36e841f3f871..3f02772b46bb3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { PropertiesRight } from './properties_right'; import { useKibana } from '../../../../common/lib/kibana'; import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; -import { disableTemplate } from '../../../../../common/constants'; jest.mock('../../../../common/lib/kibana', () => { return { @@ -97,20 +96,10 @@ describe('Properties Right', () => { expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toBeTruthy(); }); - test('it renders create timelin btn', () => { + test('it renders create timeline btn', () => { expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy(); }); - /* - * CreateTemplateTimelineBtn - * Remove the comment here to enable CreateTemplateTimelineBtn - */ - test('it renders no create template timelin btn', () => { - expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual( - !disableTemplate - ); - }); - test('it renders create attach timeline to a case btn', () => { expect(wrapper.find('[data-test-subj="NewCase"]').exists()).toBeTruthy(); }); @@ -208,14 +197,8 @@ describe('Properties Right', () => { expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toBeTruthy(); }); - test('it renders no create timelin btn', () => { - expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).not.toBeTruthy(); - }); - - test('it renders create template timelin btn if it is enabled', () => { - expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual( - !disableTemplate - ); + test('it renders create timeline template btn', () => { + expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual(true); }); test('it renders create attach timeline to a case btn', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx index 8a1bf0a842cb0..70257c97a6887 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx @@ -16,9 +16,11 @@ import { } from '@elastic/eui'; import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers'; -import { disableTemplate } from '../../../../../common/constants'; -import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline'; - +import { + TimelineStatusLiteral, + TimelineTypeLiteral, + TimelineType, +} from '../../../../../common/types/timeline'; import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; import { useKibana } from '../../../../common/lib/kibana'; import { Note } from '../../../../common/lib/note'; @@ -151,41 +153,39 @@ const PropertiesRightComponent: React.FC = ({ )} - {/* - * CreateTemplateTimelineBtn - * Remove the comment here to enable CreateTemplateTimelineBtn - */} - {!disableTemplate && ( - - - - )} - - - - - - + - + + {timelineType === TimelineType.default && ( + <> + + + + + + + + )} + {i18n.ALERT_EVENT}, + inputDisplay: {i18n.DETECTION_ALERTS_EVENT}, }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 388085d1361f3..4d90bd875efcc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import { @@ -117,57 +117,64 @@ export const SearchOrFilter = React.memo( updateEventType, updateKqlMode, updateReduxTime, - }) => ( - <> - - - - - updateKqlMode({ id: timelineId, kqlMode: mode })} - options={options} - popoverClassName={searchOrFilterPopoverClassName} - valueOfSelected={kqlMode} + }) => { + const handleChange = useCallback( + (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }), + [timelineId, updateKqlMode] + ); + + return ( + <> + + + + + + + + + - - - - - - - - - - - - - ) + + + + + + + + + ); + } ); SearchOrFilter.displayName = 'SearchOrFilter'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts index 7271c599302c5..7fa520a2d8df4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts @@ -84,9 +84,9 @@ export const RAW_EVENT = i18n.translate( } ); -export const ALERT_EVENT = i18n.translate( - 'xpack.securitySolution.timeline.searchOrFilter.eventTypeAlertEvent', +export const DETECTION_ALERTS_EVENT = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.eventTypeDetectionAlertsEvent', { - defaultMessage: 'Alert events', + defaultMessage: 'Detection Alerts', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx index b549fdab8ea4a..825d4fe3b29b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx @@ -52,7 +52,7 @@ const SearchTimelineSuperSelectComponent: React.FC { const [isPopoverOpen, setIsPopoverOpen] = useState(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx index 0ff4c0a70fff2..6bea5a7b7635e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx @@ -60,7 +60,7 @@ describe('SelectableTimeline', () => { }); }); - describe('template timeline', () => { + describe('timeline template', () => { const templateTimelineProps = { ...props, timelineType: TimelineType.template }; beforeAll(() => { wrapper = shallow(); @@ -74,7 +74,7 @@ describe('SelectableTimeline', () => { const searchProps: SearchProps = wrapper .find('[data-test-subj="selectable-input"]') .prop('searchProps'); - expect(searchProps.placeholder).toEqual('e.g. Template timeline name or description'); + expect(searchProps.placeholder).toEqual('e.g. Timeline template name or description'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index dacaf325130d7..ae8bf53090789 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -33,7 +33,6 @@ import * as i18nTimeline from '../../open_timeline/translations'; import { OpenTimelineResult } from '../../open_timeline/types'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import * as i18n from '../translations'; -import { useTimelineStatus } from '../../open_timeline/use_timeline_status'; const MyEuiFlexItem = styled(EuiFlexItem)` display: inline-block; @@ -119,7 +118,6 @@ const SelectableTimelineComponent: React.FC = ({ const [onlyFavorites, setOnlyFavorites] = useState(false); const [searchRef, setSearchRef] = useState(null); const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline(); - const { timelineStatus, templateTimelineType } = useTimelineStatus({ timelineType }); const onSearchTimeline = useCallback((val) => { setSearchTimelineValue(val); @@ -263,19 +261,11 @@ const SelectableTimelineComponent: React.FC = ({ sortOrder: Direction.desc, }, onlyUserFavorite: onlyFavorites, - status: timelineStatus, + status: null, timelineType, - templateTimelineType, + templateTimelineType: null, }); - }, [ - fetchAllTimeline, - onlyFavorites, - pageSize, - searchTimelineValue, - timelineType, - timelineStatus, - templateTimelineType, - ]); + }, [fetchAllTimeline, onlyFavorites, pageSize, searchTimelineValue, timelineType]); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index b58505546c341..360737ce41d2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -24,7 +24,7 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; -import { TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; jest.mock('../../../common/lib/kibana'); jest.mock('./properties/properties_right'); @@ -82,6 +82,7 @@ describe('Timeline', () => { indexPattern, indexToAdd: [], isLive: false, + isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], kqlMode: 'search' as TimelineComponentProps['kqlMode'], @@ -93,6 +94,7 @@ describe('Timeline', () => { onDataProviderRemoved: jest.fn(), onToggleDataProviderEnabled: jest.fn(), onToggleDataProviderExcluded: jest.fn(), + onToggleDataProviderType: jest.fn(), show: true, showCallOutUnauthorizedMsg: false, start: startDate, @@ -100,6 +102,7 @@ describe('Timeline', () => { status: TimelineStatus.active, toggleColumn: jest.fn(), usersViewing: ['elastic'], + timelineType: TimelineType.default, }; }); @@ -298,9 +301,9 @@ describe('Timeline', () => { ); const andProviderBadgesText = andProviderBadges.map((node) => node.text()).join(' '); - expect(andProviderBadges.length).toEqual(6); + expect(andProviderBadges.length).toEqual(3); expect(andProviderBadgesText).toEqual( - 'name: "Provider 1" name: "Provider 2" name: "Provider 3"' + 'name: "Provider 1" name: "Provider 2" name: "Provider 3"' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index b930325c3d35d..ee48f97164b86 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiProgress } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; import React, { useState, useMemo, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -27,12 +27,14 @@ import { OnDataProviderEdited, OnToggleDataProviderEnabled, OnToggleDataProviderExcluded, + OnToggleDataProviderType, } from './events'; import { TimelineKqlFetch } from './fetch_kql_timeline'; import { Footer, footerHeight } from './footer'; import { TimelineHeader } from './header'; import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; +import { TIMELINE_TEMPLATE } from './translations'; import { esQuery, Filter, @@ -40,12 +42,13 @@ import { IIndexPattern, } from '../../../../../../../src/plugins/data/public'; import { useManageTimeline } from '../manage_timeline'; -import { TimelineStatusLiteral } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatusLiteral } from '../../../../common/types/timeline'; const TimelineContainer = styled.div` height: 100%; display: flex; flex-direction: column; + position: relative; `; const TimelineHeaderContainer = styled.div` @@ -84,6 +87,13 @@ const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` padding: 0 10px 5px 12px; `; +const TimelineTemplateBadge = styled.div` + background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; + color: #fff; + padding: 10px 15px; + font-size: 0.8em; +`; + export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -96,6 +106,7 @@ export interface Props { indexPattern: IIndexPattern; indexToAdd: string[]; isLive: boolean; + isSaving: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; kqlMode: KqlMode; @@ -107,6 +118,7 @@ export interface Props { onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + onToggleDataProviderType: OnToggleDataProviderType; show: boolean; showCallOutUnauthorizedMsg: boolean; start: number; @@ -114,6 +126,7 @@ export interface Props { status: TimelineStatusLiteral; toggleColumn: (column: ColumnHeaderOptions) => void; usersViewing: string[]; + timelineType: TimelineType; } /** The parent Timeline component */ @@ -129,6 +142,7 @@ export const TimelineComponent: React.FC = ({ indexPattern, indexToAdd, isLive, + isSaving, itemsPerPage, itemsPerPageOptions, kqlMode, @@ -140,11 +154,13 @@ export const TimelineComponent: React.FC = ({ onDataProviderRemoved, onToggleDataProviderEnabled, onToggleDataProviderExcluded, + onToggleDataProviderType, show, showCallOutUnauthorizedMsg, start, status, sort, + timelineType, toggleColumn, usersViewing, }) => { @@ -182,6 +198,7 @@ export const TimelineComponent: React.FC = ({ }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingIndexName }); }, [loadingIndexName, id, isQueryLoading, setIsTimelineLoading]); @@ -192,6 +209,10 @@ export const TimelineComponent: React.FC = ({ return ( + {isSaving && } + {timelineType === TimelineType.template && ( + {TIMELINE_TEMPLATE} + )} = ({ = ({ onDataProviderRemoved={onDataProviderRemoved} onToggleDataProviderEnabled={onToggleDataProviderEnabled} onToggleDataProviderExcluded={onToggleDataProviderExcluded} + onToggleDataProviderType={onToggleDataProviderType} show={show} showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg} + timelineId={id} status={status} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts index ebd27f9bffa5e..f8c38b3527d7a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts @@ -23,7 +23,7 @@ export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate( export const SEARCH_BOX_TIMELINE_PLACEHOLDER = (timelineType: TimelineTypeLiteral) => i18n.translate('xpack.securitySolution.timeline.searchBoxPlaceholder', { - values: { timeline: timelineType === TimelineType.template ? 'Template timeline' : 'Timeline' }, + values: { timeline: timelineType === TimelineType.template ? 'Timeline template' : 'Timeline' }, defaultMessage: 'e.g. {timeline} name or description', }); @@ -33,3 +33,10 @@ export const INSERT_TIMELINE = i18n.translate( defaultMessage: 'Insert timeline link', } ); + +export const TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.flyoutTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index 17cc0f64de039..4ecabeef16dff 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -23,6 +23,7 @@ import { useApolloClient } from '../../../common/utils/apollo_context'; import { allTimelinesQuery } from './index.gql_query'; import * as i18n from '../../pages/translations'; import { + TimelineType, TimelineTypeLiteralWithNull, TimelineStatusLiteralWithNull, TemplateTimelineTypeLiteralWithNull, @@ -92,6 +93,7 @@ export const getAllTimeline = memoizeOne( title: timeline.title, updated: timeline.updated, updatedBy: timeline.updatedBy, + timelineType: timeline.timelineType ?? TimelineType.default, })) ); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index 089a428f7dfaf..42c01da7e23c9 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -7,7 +7,7 @@ import * as api from './api'; import { KibanaServices } from '../../common/lib/kibana'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { TIMELINE_DRAFT_URL, TIMELINE_URL } from '../../../common/constants'; -import { ImportDataProps } from '../../alerts/containers/detection_engine/rules/types'; +import { ImportDataProps } from '../../detections/containers/detection_engine/rules/types'; jest.mock('../../common/lib/kibana', () => { return { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index ff252ea93039d..72e1f1d4de32d 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -30,7 +30,7 @@ import { createToasterPlainError } from '../../cases/containers/utils'; import { ImportDataProps, ImportDataResponse, -} from '../../alerts/containers/detection_engine/rules'; +} from '../../detections/containers/detection_engine/rules'; interface RequestPostTimeline { timeline: TimelineInput; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 6a6d74cc91508..164d34db16d87 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -29,7 +29,7 @@ import { EventType } from '../../timelines/store/timeline/model'; import { timelineQuery } from './index.gql_query'; import { timelineActions } from '../../timelines/store/timeline'; -const timelineIds = [TimelineId.alertsPage, TimelineId.alertsRulesDetailsPage]; +const timelineIds = [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage]; export interface TimelineArgs { events: TimelineItem[]; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index 47e80b005fb99..24beed0801aa6 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -28,6 +28,7 @@ export const oneTimelineQuery = gql` enabled excluded kqlQuery + type queryMatch { field displayField diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index 1bd5874394df3..2e59dbb72233f 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -9,12 +9,23 @@ import React from 'react'; import { useKibana } from '../../common/lib/kibana'; import { TimelinesPageComponent } from './timelines_page'; -import { disableTemplate } from '../../../common/constants'; -jest.mock('../../overview/components/events_by_dataset'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useParams: jest.fn().mockReturnValue({ + tabName: 'default', + }), + }; +}); +jest.mock('../../overview/components/events_by_dataset'); jest.mock('../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../common/lib/kibana'); + return { + ...originalModule, useKibana: jest.fn(), }; }); @@ -59,22 +70,16 @@ describe('TimelinesPageComponent', () => { ).toEqual(true); }); - test('it renders create timelin btn', () => { + test('it renders create timeline btn', () => { expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy(); }); - /* - * CreateTemplateTimelineBtn - * Remove the comment here to enable CreateTemplateTimelineBtn - */ - test('it renders no create template timelin btn', () => { - expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual( - !disableTemplate - ); + test('it renders no create timeline template btn', () => { + expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toBeFalsy(); }); }); - describe('If the user is not authorised', () => { + describe('If the user is not authorized', () => { beforeAll(() => { ((useKibana as unknown) as jest.Mock).mockReturnValue({ services: { 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 089a928403b0b..56aff3ec8aaac 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 @@ -7,9 +7,9 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; +import { useParams } from 'react-router-dom'; -import { disableTemplate } from '../../../common/constants'; - +import { TimelineType } from '../../../common/types/timeline'; import { HeaderPage } from '../../common/components/header_page'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; @@ -31,6 +31,7 @@ const TimelinesContainer = styled.div` export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPageComponent: React.FC = () => { + const { tabName } = useParams(); const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); @@ -56,20 +57,17 @@ export const TimelinesPageComponent: React.FC = () => { )} - - {capabilitiesCanUserCRUD && ( - - )} - - {/** - * CreateTemplateTimelineBtn - * Remove the comment here to enable CreateTemplateTimelineBtn - */} - {!disableTemplate && ( + {tabName === TimelineType.default ? ( + + {capabilitiesCanUserCRUD && ( + + )} + + ) : ( ('PROVIDER_EDIT_KQL_QUERY'); +export const updateDataProviderType = actionCreator<{ + andProviderId?: string; + id: string; + type: DataProviderType; + providerId: string; +}>('UPDATE_PROVIDER_TYPE'); + export const updateHighlightedDropAndProviderId = actionCreator<{ id: string; providerId: string; 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 94acb9d92075b..605700cb71a2a 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 @@ -58,6 +58,7 @@ import { updateDataProviderEnabled, updateDataProviderExcluded, updateDataProviderKqlQuery, + updateDataProviderType, updateDescription, updateKqlMode, updateProviders, @@ -96,6 +97,7 @@ const timelineActionsType = [ updateDataProviderEnabled.type, updateDataProviderExcluded.type, updateDataProviderKqlQuery.type, + updateDataProviderType.type, updateDescription.type, updateEventType.type, updateKqlMode.type, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 388869194085c..7d65181db65fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -39,7 +39,7 @@ import { Direction } from '../../../graphql/types'; import { addTimelineInStorage } from '../../containers/local_storage'; import { isPageTimeline } from './epic_local_storage'; -import { TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; jest.mock('../../containers/local_storage'); @@ -89,6 +89,7 @@ describe('epicLocalStorage', () => { indexPattern, indexToAdd: [], isLive: false, + isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], kqlMode: 'search' as TimelineComponentProps['kqlMode'], @@ -100,11 +101,13 @@ describe('epicLocalStorage', () => { onDataProviderRemoved: jest.fn(), onToggleDataProviderEnabled: jest.fn(), onToggleDataProviderExcluded: jest.fn(), + onToggleDataProviderType: jest.fn(), show: true, showCallOutUnauthorizedMsg: false, start: startDate, status: TimelineStatus.active, sort, + timelineType: TimelineType.default, toggleColumn: jest.fn(), usersViewing: ['elastic'], }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 33770aacde6bb..a347d3e41e206 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -9,14 +9,15 @@ import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; import uuid from 'uuid'; import { Filter } from '../../../../../../../src/plugins/data/public'; -import { disableTemplate } from '../../../../common/constants'; - import { getColumnWidthFromType } from '../../../timelines/components/timeline/body/column_headers/helpers'; import { Sort } from '../../../timelines/components/timeline/body/sort'; import { DataProvider, QueryOperator, QueryMatch, + DataProviderType, + IS_OPERATOR, + EXISTS_OPERATOR, } from '../../../timelines/components/timeline/data_providers/data_provider'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; import { TimelineNonEcsData } from '../../../graphql/types'; @@ -161,7 +162,7 @@ export const addNewTimeline = ({ timelineType, }: AddNewTimelineParams): TimelineById => { const templateTimelineInfo = - !disableTemplate && timelineType === TimelineType.template + timelineType === TimelineType.template ? { templateTimelineId: uuid.v4(), templateTimelineVersion: 1, @@ -186,7 +187,7 @@ export const addNewTimeline = ({ isLoading: false, showCheckboxes, showRowRenderers, - timelineType: !disableTemplate ? timelineType : timelineDefaults.timelineType, + timelineType, ...templateTimelineInfo, }, }; @@ -1046,6 +1047,92 @@ export const updateTimelineProviderKqlQuery = ({ }; }; +interface UpdateTimelineProviderTypeParams { + andProviderId?: string; + id: string; + providerId: string; + type: DataProviderType; + timelineById: TimelineById; +} + +const updateTypeAndProvider = ( + andProviderId: string, + type: DataProviderType, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map((provider) => + provider.id === providerId + ? { + ...provider, + and: provider.and.map((andProvider) => + andProvider.id === andProviderId + ? { + ...andProvider, + type, + name: type === DataProviderType.template ? `${andProvider.queryMatch.field}` : '', + queryMatch: { + ...andProvider.queryMatch, + displayField: undefined, + displayValue: undefined, + value: + type === DataProviderType.template ? `{${andProvider.queryMatch.field}}` : '', + operator: (type === DataProviderType.template + ? IS_OPERATOR + : EXISTS_OPERATOR) as QueryOperator, + }, + } + : andProvider + ), + } + : provider + ); + +const updateTypeProvider = (type: DataProviderType, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map((provider) => + provider.id === providerId + ? { + ...provider, + type, + name: type === DataProviderType.template ? `${provider.queryMatch.field}` : '', + queryMatch: { + ...provider.queryMatch, + displayField: undefined, + displayValue: undefined, + value: type === DataProviderType.template ? `{${provider.queryMatch.field}}` : '', + operator: (type === DataProviderType.template + ? IS_OPERATOR + : EXISTS_OPERATOR) as QueryOperator, + }, + } + : provider + ); + +export const updateTimelineProviderType = ({ + andProviderId, + id, + providerId, + type, + timelineById, +}: UpdateTimelineProviderTypeParams): TimelineById => { + const timeline = timelineById[id]; + + if (timeline.timelineType !== TimelineType.template && type === DataProviderType.template) { + // Not supported, timeline template cannot have template type providers + return timelineById; + } + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateTypeAndProvider(andProviderId, type, providerId, timeline) + : updateTypeProvider(type, providerId, timeline), + }, + }; +}; + interface UpdateTimelineItemsPerPageParams { id: string; itemsPerPage: number; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 57895fea8f8ff..a78fbc41ac430 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -87,9 +87,9 @@ export interface TimelineModel { title: string; /** timelineType: default | template */ timelineType: TimelineType; - /** an unique id for template timeline */ + /** an unique id for timeline template */ templateTimelineId: string | null; - /** null for default timeline, number for template timeline */ + /** null for default timeline, number for timeline template */ templateTimelineVersion: number | null; /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ noteIds: string[]; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 6e7a36079a0c3..b8bdb4f2ad7f0 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -11,6 +11,7 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline' import { IS_OPERATOR, DataProvider, + DataProviderType, DataProvidersAnd, } from '../../../timelines/components/timeline/data_providers/data_provider'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; @@ -35,6 +36,7 @@ import { updateTimelinePerPageOptions, updateTimelineProviderEnabled, updateTimelineProviderExcluded, + updateTimelineProviderType, updateTimelineProviders, updateTimelineRange, updateTimelineShowTimeline, @@ -107,6 +109,14 @@ const timelineByIdMock: TimelineById = { }, }; +const timelineByIdTemplateMock: TimelineById = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + timelineType: TimelineType.template, + }, +}; + const columnsMock: ColumnHeaderOptions[] = [ defaultHeaders[0], defaultHeaders[1], @@ -1547,6 +1557,211 @@ describe('Timeline', () => { }); }); + describe('#updateTimelineProviderType', () => { + test('should return the same reference if run on timelineType default', () => { + const update = updateTimelineProviderType({ + id: 'foo', + providerId: '123', + type: DataProviderType.template, // value we are updating from default to template + timelineById: timelineByIdMock, + }); + expect(update).toBe(timelineByIdMock); + }); + + test('should return a new reference and not the same reference', () => { + const update = updateTimelineProviderType({ + id: 'foo', + providerId: '123', + type: DataProviderType.template, // value we are updating from default to template + timelineById: timelineByIdTemplateMock, + }); + expect(update).not.toBe(timelineByIdTemplateMock); + }); + + test('should return a new reference for data provider and not the same reference of data provider', () => { + const update = updateTimelineProviderType({ + id: 'foo', + providerId: '123', + type: DataProviderType.template, // value we are updating from default to template + timelineById: timelineByIdTemplateMock, + }); + expect(update.foo.dataProviders).not.toBe(timelineByIdTemplateMock.foo.dataProviders); + }); + + test('should update the timeline provider type from default to template', () => { + const update = updateTimelineProviderType({ + id: 'foo', + providerId: '123', + type: DataProviderType.template, // value we are updating from default to template + timelineById: timelineByIdTemplateMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: '', // This value changed + enabled: true, + excluded: false, + kqlQuery: '', + type: DataProviderType.template, // value we are updating from default to template + queryMatch: { + field: '', + value: '{}', // This value changed + operator: IS_OPERATOR, + }, + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.template, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + status: TimelineStatus.active, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + + test('should update only one data provider and not two data providers', () => { + const multiDataProvider = timelineByIdTemplateMock.foo.dataProviders.concat({ + and: [], + id: '456', + name: 'data provider 1', + enabled: true, + excluded: false, + type: DataProviderType.template, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }); + const multiDataProviderMock = set( + 'foo.dataProviders', + multiDataProvider, + timelineByIdTemplateMock + ); + const update = updateTimelineProviderType({ + id: 'foo', + providerId: '123', + type: DataProviderType.template, // value we are updating from default to template + timelineById: multiDataProviderMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: '', + enabled: true, + excluded: false, + type: DataProviderType.template, // value we are updating from default to template + kqlQuery: '', + queryMatch: { + field: '', + value: '{}', + operator: IS_OPERATOR, + }, + }, + { + and: [], + id: '456', + name: 'data provider 1', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + type: DataProviderType.template, + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.template, + templateTimelineId: null, + templateTimelineVersion: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + status: TimelineStatus.active, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + }); + describe('#updateTimelineAndProviderExcluded', () => { let timelineByIdwithAndMock: TimelineById = timelineByIdMock; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 4072b4ac2f78b..6bb546c16b617 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -39,6 +39,7 @@ import { updateDataProviderEnabled, updateDataProviderExcluded, updateDataProviderKqlQuery, + updateDataProviderType, updateDescription, updateEventType, updateHighlightedDropAndProviderId, @@ -88,6 +89,7 @@ import { updateTimelineProviderExcluded, updateTimelineProviderProperties, updateTimelineProviderKqlQuery, + updateTimelineProviderType, updateTimelineProviders, updateTimelineRange, updateTimelineShowTimeline, @@ -427,7 +429,16 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }), }) ) - + .case(updateDataProviderType, (state, { id, type, providerId, andProviderId }) => ({ + ...state, + timelineById: updateTimelineProviderType({ + id, + type, + providerId, + timelineById: state.timelineById, + andProviderId, + }), + })) .case(updateDataProviderKqlQuery, (state, { id, kqlQuery, providerId }) => ({ ...state, timelineById: updateTimelineProviderKqlQuery({ diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts index 2daf259941cbf..7642db23812e1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts @@ -8,14 +8,14 @@ import { httpServerMock } from '../../../../../src/core/server/mocks'; import { EndpointAppContextService } from './endpoint_app_context_services'; describe('test endpoint app context services', () => { - it('should throw error on getAgentService if start is not called', async () => { - const endpointAppContextService = new EndpointAppContextService(); - expect(() => endpointAppContextService.getAgentService()).toThrow(Error); - }); - it('should return undefined on getManifestManager if start is not called', async () => { - const endpointAppContextService = new EndpointAppContextService(); - expect(endpointAppContextService.getManifestManager()).toEqual(undefined); - }); + // it('should return undefined on getAgentService if dependencies are not enabled', async () => { + // const endpointAppContextService = new EndpointAppContextService(); + // expect(endpointAppContextService.getAgentService()).toEqual(undefined); + // }); + // it('should return undefined on getManifestManager if dependencies are not enabled', async () => { + // const endpointAppContextService = new EndpointAppContextService(); + // expect(endpointAppContextService.getManifestManager()).toEqual(undefined); + // }); it('should throw error on getScopedSavedObjectsClient if start is not called', async () => { const endpointAppContextService = new EndpointAppContextService(); expect(() => diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 97a82049634c4..f51e8c6be1040 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { - SavedObjectsServiceStart, KibanaRequest, + Logger, + SavedObjectsServiceStart, SavedObjectsClientContract, } from 'src/core/server'; import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; import { getPackageConfigCreateCallback } from './ingest_integration'; import { ManifestManager } from './services/artifacts'; -export type EndpointAppContextServiceStartContract = Pick< - IngestManagerStartContract, - 'agentService' +export type EndpointAppContextServiceStartContract = Partial< + Pick > & { - manifestManager?: ManifestManager | undefined; - registerIngestCallback: IngestManagerStartContract['registerExternalCallback']; + logger: Logger; + manifestManager?: ManifestManager; + registerIngestCallback?: IngestManagerStartContract['registerExternalCallback']; savedObjectsStart: SavedObjectsServiceStart; }; @@ -35,20 +36,17 @@ export class EndpointAppContextService { this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; - if (this.manifestManager !== undefined) { + if (this.manifestManager && dependencies.registerIngestCallback) { dependencies.registerIngestCallback( 'packageConfigCreate', - getPackageConfigCreateCallback(this.manifestManager) + getPackageConfigCreateCallback(dependencies.logger, this.manifestManager) ); } } public stop() {} - public getAgentService(): AgentService { - if (!this.agentService) { - throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); - } + public getAgentService(): AgentService | undefined { return this.agentService; } diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index ace5aec77ed2c..1acec1e7c53ac 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../src/core/server'; import { NewPackageConfig } from '../../../ingest_manager/common/types/models'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; @@ -13,6 +14,7 @@ import { ManifestManager } from './services/artifacts'; * Callback to handle creation of PackageConfigs in Ingest Manager */ export const getPackageConfigCreateCallback = ( + logger: Logger, manifestManager: ManifestManager ): ((newPackageConfig: NewPackageConfig) => Promise) => { const handlePackageConfigCreate = async ( @@ -27,8 +29,19 @@ export const getPackageConfigCreateCallback = ( // follow the types/schema expected let updatedPackageConfig = newPackageConfig as NewPolicyData; - const wrappedManifest = await manifestManager.refresh({ initialize: true }); - if (wrappedManifest !== null) { + // get snapshot based on exception-list-agnostic SOs + // with diffs from last dispatched manifest, if it exists + const snapshot = await manifestManager.getSnapshot({ initialize: true }); + + if (snapshot === null) { + logger.warn('No manifest snapshot available.'); + return updatedPackageConfig; + } + + if (snapshot.diffs.length > 0) { + // create new artifacts + await manifestManager.syncArtifacts(snapshot, 'add'); + // Until we get the Default Policy Configuration in the Endpoint package, // we will add it here manually at creation time. // @ts-ignore @@ -42,7 +55,7 @@ export const getPackageConfigCreateCallback = ( streams: [], config: { artifact_manifest: { - value: wrappedManifest.manifest.toEndpointFormat(), + value: snapshot.manifest.toEndpointFormat(), }, policy: { value: policyConfigFactory(), @@ -57,9 +70,18 @@ export const getPackageConfigCreateCallback = ( try { return updatedPackageConfig; } finally { - // TODO: confirm creation of package config - // then commit. - await manifestManager.commit(wrappedManifest); + if (snapshot.diffs.length > 0) { + // TODO: let's revisit the way this callback happens... use promises? + // only commit when we know the package config was created + try { + await manifestManager.commit(snapshot.manifest); + + // clean up old artifacts + await manifestManager.syncArtifacts(snapshot, 'delete'); + } catch (err) { + logger.error(err); + } + } } }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts index 5a0fb91345552..00c764d0b912e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts @@ -8,6 +8,7 @@ import { ExceptionsCache } from './cache'; describe('ExceptionsCache tests', () => { let cache: ExceptionsCache; + const body = Buffer.from('body'); beforeEach(() => { jest.clearAllMocks(); @@ -15,29 +16,33 @@ describe('ExceptionsCache tests', () => { }); test('it should cache', async () => { - cache.set('test', 'body'); + cache.set('test', body); const cacheResp = cache.get('test'); - expect(cacheResp).toEqual('body'); + expect(cacheResp).toEqual(body); }); test('it should handle cache miss', async () => { - cache.set('test', 'body'); + cache.set('test', body); const cacheResp = cache.get('not test'); expect(cacheResp).toEqual(undefined); }); test('it should handle cache eviction', async () => { - cache.set('1', 'a'); - cache.set('2', 'b'); - cache.set('3', 'c'); + const a = Buffer.from('a'); + const b = Buffer.from('b'); + const c = Buffer.from('c'); + const d = Buffer.from('d'); + cache.set('1', a); + cache.set('2', b); + cache.set('3', c); const cacheResp = cache.get('1'); - expect(cacheResp).toEqual('a'); + expect(cacheResp).toEqual(a); - cache.set('4', 'd'); + cache.set('4', d); const secondResp = cache.get('1'); expect(secondResp).toEqual(undefined); - expect(cache.get('2')).toEqual('b'); - expect(cache.get('3')).toEqual('c'); - expect(cache.get('4')).toEqual('d'); + expect(cache.get('2')).toEqual(b); + expect(cache.get('3')).toEqual(c); + expect(cache.get('4')).toEqual(d); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts index b7a4c2feb6bf8..b9d3bae4e6ef9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts @@ -10,7 +10,7 @@ const DEFAULT_MAX_SIZE = 10; * FIFO cache implementation for artifact downloads. */ export class ExceptionsCache { - private cache: Map; + private cache: Map; private queue: string[]; private maxSize: number; @@ -20,7 +20,7 @@ export class ExceptionsCache { this.maxSize = maxSize || DEFAULT_MAX_SIZE; } - set(id: string, body: string) { + set(id: string, body: Buffer) { if (this.queue.length + 1 > this.maxSize) { const entry = this.queue.shift(); if (entry !== undefined) { @@ -31,7 +31,7 @@ export class ExceptionsCache { this.cache.set(id, body); } - get(id: string): string | undefined { + get(id: string): Buffer | undefined { return this.cache.get(id); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index cf38147522083..9ad4554b30203 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -8,10 +8,11 @@ export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', SAVED_OBJECT_TYPE: 'endpoint:user-artifact:v2', SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'], - SCHEMA_VERSION: '1.0.0', + SCHEMA_VERSION: 'v1', }; export const ManifestConstants = { SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest:v2', - SCHEMA_VERSION: '1.0.0', + SCHEMA_VERSION: 'v1', + INITIAL_VERSION: 'WzAsMF0=', }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 0a1cd556e6e91..acde455f77cb4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -46,7 +46,7 @@ describe('buildEventTypeSignal', () => { const first = getFoundExceptionListItemSchemaMock(); mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -87,7 +87,7 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -133,7 +133,7 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -171,7 +171,7 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -193,7 +193,7 @@ describe('buildEventTypeSignal', () => { .mockReturnValueOnce(first) .mockReturnValueOnce(second) .mockReturnValueOnce(third); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp.entries.length).toEqual(3); }); @@ -202,7 +202,7 @@ describe('buildEventTypeSignal', () => { exceptionsResponse.data = []; exceptionsResponse.total = 0; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp.entries.length).toEqual(0); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index a13781519b508..556405adff62f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -5,6 +5,7 @@ */ import { createHash } from 'crypto'; +import { deflate } from 'zlib'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; @@ -34,6 +35,7 @@ export async function buildArtifact( const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); + // Keep compression info empty in case its a duplicate. Lazily compress before committing if needed. return { identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`, compressionAlgorithm: 'none', @@ -95,7 +97,7 @@ export function translateToEndpointExceptions( exc: FoundExceptionListItemSchema, schemaVersion: string ): TranslatedExceptionListItem[] { - if (schemaVersion === '1.0.0') { + if (schemaVersion === 'v1') { return exc.data.map((item) => { return translateItem(schemaVersion, item); }); @@ -180,3 +182,15 @@ function translateEntry( } } } + +export async function compressExceptionList(buffer: Buffer): Promise { + return new Promise((resolve, reject) => { + deflate(buffer, function (err, buf) { + if (err) { + reject(err); + } else { + resolve(buf); + } + }); + }); +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 3e5fdbf9484ca..e1f6bac2620ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -10,6 +10,7 @@ import { getInternalArtifactMock, getInternalArtifactMockWithDiffs, } from '../../schemas/artifacts/saved_objects.mock'; +import { ManifestConstants } from './common'; import { Manifest } from './manifest'; describe('manifest', () => { @@ -20,41 +21,45 @@ describe('manifest', () => { let manifest2: Manifest; beforeAll(async () => { - const artifactLinux = await getInternalArtifactMock('linux', '1.0.0'); - const artifactMacos = await getInternalArtifactMock('macos', '1.0.0'); - const artifactWindows = await getInternalArtifactMock('windows', '1.0.0'); + const artifactLinux = await getInternalArtifactMock('linux', 'v1'); + const artifactMacos = await getInternalArtifactMock('macos', 'v1'); + const artifactWindows = await getInternalArtifactMock('windows', 'v1'); artifacts.push(artifactLinux); artifacts.push(artifactMacos); artifacts.push(artifactWindows); - manifest1 = new Manifest(now, '1.0.0', 'v0'); + manifest1 = new Manifest(now, 'v1', ManifestConstants.INITIAL_VERSION); manifest1.addEntry(artifactLinux); manifest1.addEntry(artifactMacos); manifest1.addEntry(artifactWindows); manifest1.setVersion('abcd'); - const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', '1.0.0'); - manifest2 = new Manifest(new Date(), '1.0.0', 'v0'); + const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', 'v1'); + manifest2 = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION); manifest2.addEntry(newArtifactLinux); manifest2.addEntry(artifactMacos); manifest2.addEntry(artifactWindows); }); test('Can create manifest with valid schema version', () => { - const manifest = new Manifest(new Date(), '1.0.0', 'v0'); + const manifest = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION); expect(manifest).toBeInstanceOf(Manifest); }); test('Cannot create manifest with invalid schema version', () => { expect(() => { - new Manifest(new Date(), 'abcd' as ManifestSchemaVersion, 'v0'); + new Manifest( + new Date(), + 'abcd' as ManifestSchemaVersion, + ManifestConstants.INITIAL_VERSION + ); }).toThrow(); }); test('Manifest transforms correctly to expected endpoint format', async () => { expect(manifest1.toEndpointFormat()).toStrictEqual({ artifacts: { - 'endpoint-exceptionlist-linux-1.0.0': { + 'endpoint-exceptionlist-linux-v1': { compression_algorithm: 'none', encryption_algorithm: 'none', decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', @@ -62,9 +67,9 @@ describe('manifest', () => { decoded_size: 430, encoded_size: 430, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', }, - 'endpoint-exceptionlist-macos-1.0.0': { + 'endpoint-exceptionlist-macos-v1': { compression_algorithm: 'none', encryption_algorithm: 'none', decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', @@ -72,9 +77,9 @@ describe('manifest', () => { decoded_size: 430, encoded_size: 430, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', }, - 'endpoint-exceptionlist-windows-1.0.0': { + 'endpoint-exceptionlist-windows-v1': { compression_algorithm: 'none', encryption_algorithm: 'none', decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', @@ -82,11 +87,11 @@ describe('manifest', () => { decoded_size: 430, encoded_size: 430, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', }, }, manifest_version: 'abcd', - schema_version: '1.0.0', + schema_version: 'v1', }); }); @@ -94,9 +99,9 @@ describe('manifest', () => { expect(manifest1.toSavedObject()).toStrictEqual({ created: now.getTime(), ids: [ - 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', ], }); }); @@ -106,12 +111,12 @@ describe('manifest', () => { expect(diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', type: 'delete', }, { id: - 'endpoint-exceptionlist-linux-1.0.0-3d3546e94f70493021ee845be32c66e36ea7a720c64b4d608d8029fe949f7e51', + 'endpoint-exceptionlist-linux-v1-3d3546e94f70493021ee845be32c66e36ea7a720c64b4d608d8029fe949f7e51', type: 'add', }, ]); @@ -119,7 +124,7 @@ describe('manifest', () => { test('Manifest returns data for given artifact', async () => { const artifact = artifacts[0]; - const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.encodedSha256}`); + const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.decodedSha256}`); expect(returned).toEqual(artifact); }); @@ -127,34 +132,39 @@ describe('manifest', () => { const entries = manifest1.getEntries(); const keys = Object.keys(entries); expect(keys).toEqual([ - 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', ]); }); test('Manifest returns true if contains artifact', async () => { const found = manifest1.contains( - 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' ); expect(found).toEqual(true); }); test('Manifest can be created from list of artifacts', async () => { - const manifest = Manifest.fromArtifacts(artifacts, '1.0.0', 'v0'); + const oldManifest = new Manifest( + new Date(), + ManifestConstants.SCHEMA_VERSION, + ManifestConstants.INITIAL_VERSION + ); + const manifest = Manifest.fromArtifacts(artifacts, 'v1', oldManifest); expect( manifest.contains( - 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' ) ).toEqual(true); expect( manifest.contains( - 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' ) ).toEqual(true); expect( manifest.contains( - 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' ) ).toEqual(true); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts index c343568226e22..576ecb08d6923 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -11,6 +11,7 @@ import { ManifestSchemaVersion, } from '../../../../common/endpoint/schema/common'; import { ManifestSchema, manifestSchema } from '../../../../common/endpoint/schema/manifest'; +import { ManifestConstants } from './common'; import { ManifestEntry } from './manifest_entry'; export interface ManifestDiff { @@ -46,11 +47,17 @@ export class Manifest { public static fromArtifacts( artifacts: InternalArtifactSchema[], schemaVersion: string, - version: string + oldManifest: Manifest ): Manifest { - const manifest = new Manifest(new Date(), schemaVersion, version); + const manifest = new Manifest(new Date(), schemaVersion, oldManifest.getVersion()); artifacts.forEach((artifact) => { - manifest.addEntry(artifact); + const id = `${artifact.identifier}-${artifact.decodedSha256}`; + const existingArtifact = oldManifest.getArtifact(id); + if (existingArtifact) { + manifest.addEntry(existingArtifact); + } else { + manifest.addEntry(artifact); + } }); return manifest; } @@ -80,8 +87,8 @@ export class Manifest { return this.entries; } - public getArtifact(artifactId: string): InternalArtifactSchema { - return this.entries[artifactId].getArtifact(); + public getArtifact(artifactId: string): InternalArtifactSchema | undefined { + return this.entries[artifactId]?.getArtifact(); } public diff(manifest: Manifest): ManifestDiff[] { @@ -104,7 +111,7 @@ export class Manifest { public toEndpointFormat(): ManifestSchema { const manifestObj: ManifestSchema = { - manifest_version: this.version ?? 'v0', + manifest_version: this.version ?? ManifestConstants.INITIAL_VERSION, schema_version: this.schemaVersion, artifacts: {}, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts index a52114ad90258..7ea2a07210c55 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts @@ -14,7 +14,7 @@ describe('manifest_entry', () => { let manifestEntry: ManifestEntry; beforeAll(async () => { - artifact = await getInternalArtifactMock('windows', '1.0.0'); + artifact = await getInternalArtifactMock('windows', 'v1'); manifestEntry = new ManifestEntry(artifact); }); @@ -24,12 +24,12 @@ describe('manifest_entry', () => { test('Correct doc_id is returned', () => { expect(manifestEntry.getDocId()).toEqual( - 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' ); }); test('Correct identifier is returned', () => { - expect(manifestEntry.getIdentifier()).toEqual('endpoint-exceptionlist-windows-1.0.0'); + expect(manifestEntry.getIdentifier()).toEqual('endpoint-exceptionlist-windows-v1'); }); test('Correct sha256 is returned', () => { @@ -48,7 +48,7 @@ describe('manifest_entry', () => { test('Correct url is returned', () => { expect(manifestEntry.getUrl()).toEqual( - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' ); }); @@ -65,8 +65,10 @@ describe('manifest_entry', () => { decoded_size: 430, encoded_size: 430, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', }); }); + + // TODO: add test for entry with compression }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts index c23258c4c3ba4..b35e0c2b9ad6e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts @@ -5,6 +5,7 @@ */ import { InternalArtifactSchema } from '../../schemas/artifacts'; +import { CompressionAlgorithm } from '../../../../common/endpoint/schema/common'; import { ManifestEntrySchema } from '../../../../common/endpoint/schema/manifest'; export class ManifestEntry { @@ -15,13 +16,17 @@ export class ManifestEntry { } public getDocId(): string { - return `${this.getIdentifier()}-${this.getEncodedSha256()}`; + return `${this.getIdentifier()}-${this.getDecodedSha256()}`; } public getIdentifier(): string { return this.artifact.identifier; } + public getCompressionAlgorithm(): CompressionAlgorithm { + return this.artifact.compressionAlgorithm; + } + public getEncodedSha256(): string { return this.artifact.encodedSha256; } @@ -39,7 +44,7 @@ export class ManifestEntry { } public getUrl(): string { - return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getEncodedSha256()}`; + return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getDecodedSha256()}`; } public getArtifact(): InternalArtifactSchema { @@ -48,7 +53,7 @@ export class ManifestEntry { public getRecord(): ManifestEntrySchema { return { - compression_algorithm: 'none', + compression_algorithm: this.getCompressionAlgorithm(), encryption_algorithm: 'none', decoded_sha256: this.getDecodedSha256(), decoded_size: this.getDecodedSize(), diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index 78b60e9e61f3e..aa7f56e815d58 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -88,20 +88,22 @@ export class ManifestTask { return; } - manifestManager - .refresh() - .then((wrappedManifest) => { - if (wrappedManifest) { - return manifestManager.dispatch(wrappedManifest); - } - }) - .then((wrappedManifest) => { - if (wrappedManifest) { - return manifestManager.commit(wrappedManifest); - } - }) - .catch((err) => { - this.logger.error(err); - }); + try { + // get snapshot based on exception-list-agnostic SOs + // with diffs from last dispatched manifest + const snapshot = await manifestManager.getSnapshot(); + if (snapshot && snapshot.diffs.length > 0) { + // create new artifacts + await manifestManager.syncArtifacts(snapshot, 'add'); + // write to ingest-manager package config + await manifestManager.dispatch(snapshot.manifest); + // commit latest manifest state to user-artifact-manifest SO + await manifestManager.commit(snapshot.manifest); + // clean up old artifacts + await manifestManager.syncArtifacts(snapshot, 'delete'); + } + } catch (err) { + this.logger.error(err); + } }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index fbcd3bd130dfd..8c6faee7f7a5d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { deflateSync, inflateSync } from 'zlib'; import { ILegacyClusterClient, IRouter, @@ -29,7 +30,7 @@ import { createMockEndpointAppContextServiceStartContract } from '../../mocks'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { WrappedTranslatedExceptionList } from '../../schemas/artifacts/lists'; -const mockArtifactName = `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-windows-1.0.0`; +const mockArtifactName = `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-windows-v1`; const expectedEndpointExceptions: WrappedTranslatedExceptionList = { entries: [ { @@ -93,7 +94,6 @@ describe('test alerts route', () => { let mockScopedClient: jest.Mocked; let mockSavedObjectClient: jest.Mocked; let mockResponse: jest.Mocked; - // @ts-ignore let routeConfig: RouteConfig; let routeHandler: RequestHandler; let endpointAppContextService: EndpointAppContextService; @@ -114,8 +114,9 @@ describe('test alerts route', () => { // The authentication with the Fleet Plugin needs a separate scoped SO Client ingestSavedObjectClient = savedObjectsClientMock.create(); ingestSavedObjectClient.find.mockReturnValue(Promise.resolve(mockIngestSOResponse)); - // @ts-ignore - startContract.savedObjectsStart.getScopedClient.mockReturnValue(ingestSavedObjectClient); + (startContract.savedObjectsStart.getScopedClient as jest.Mock).mockReturnValue( + ingestSavedObjectClient + ); endpointAppContextService.start(startContract); registerDownloadExceptionListRoute( @@ -146,11 +147,11 @@ describe('test alerts route', () => { references: [], attributes: { identifier: mockArtifactName, - schemaVersion: '1.0.0', + schemaVersion: 'v1', sha256: '123456', encoding: 'application/json', created: Date.now(), - body: Buffer.from(JSON.stringify(expectedEndpointExceptions)).toString('base64'), + body: deflateSync(JSON.stringify(expectedEndpointExceptions)).toString('base64'), size: 100, }, }; @@ -163,6 +164,8 @@ describe('test alerts route', () => { path.startsWith('/api/endpoint/artifacts/download') )!; + expect(routeConfig.options).toEqual(undefined); + await routeHandler( ({ core: { @@ -176,14 +179,16 @@ describe('test alerts route', () => { ); const expectedHeaders = { - 'content-encoding': 'application/json', - 'content-disposition': `attachment; filename=${mockArtifactName}.json`, + 'content-encoding': 'identity', + 'content-disposition': `attachment; filename=${mockArtifactName}.zz`, }; expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]?.headers).toEqual(expectedHeaders); - const artifact = mockResponse.ok.mock.calls[0][0]?.body; - expect(artifact).toEqual(Buffer.from(mockArtifact.attributes.body, 'base64').toString()); + const artifact = inflateSync(mockResponse.ok.mock.calls[0][0]?.body as Buffer).toString(); + expect(artifact).toEqual( + inflateSync(Buffer.from(mockArtifact.attributes.body, 'base64')).toString() + ); }); it('should handle fetching a non-existent artifact', async () => { @@ -233,7 +238,7 @@ describe('test alerts route', () => { // Add to the download cache const mockArtifact = expectedEndpointExceptions; const cacheKey = `${mockArtifactName}-${mockSha}`; - cache.set(cacheKey, JSON.stringify(mockArtifact)); + cache.set(cacheKey, Buffer.from(JSON.stringify(mockArtifact))); // TODO: add compression here [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/artifacts/download') diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 337393e768a8f..1b364a04a4272 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -43,9 +43,7 @@ export function registerDownloadExceptionListRoute( DownloadArtifactRequestParamsSchema >(downloadArtifactRequestParamsSchema), }, - options: { tags: [] }, }, - // @ts-ignore async (context, req, res) => { let scopedSOClient: SavedObjectsClientContract; const logger = endpointContext.logFactory.get('download_exception_list'); @@ -55,19 +53,19 @@ export function registerDownloadExceptionListRoute( scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req); await authenticateAgentWithAccessToken(scopedSOClient, req); } catch (err) { - if (err.output.statusCode === 401) { + if ((err.isBoom ? err.output.statusCode : err.statusCode) === 401) { return res.unauthorized(); } else { return res.notFound(); } } - const buildAndValidateResponse = (artName: string, body: string): IKibanaResponse => { + const buildAndValidateResponse = (artName: string, body: Buffer): IKibanaResponse => { const artifact: HttpResponseOptions = { body, headers: { - 'content-encoding': 'application/json', - 'content-disposition': `attachment; filename=${artName}.json`, + 'content-encoding': 'identity', + 'content-disposition': `attachment; filename=${artName}.zz`, }, }; @@ -90,7 +88,7 @@ export function registerDownloadExceptionListRoute( return scopedSOClient .get(ArtifactConstants.SAVED_OBJECT_TYPE, id) .then((artifact: SavedObject) => { - const body = Buffer.from(artifact.attributes.body, 'base64').toString(); + const body = Buffer.from(artifact.attributes.body, 'base64'); cache.set(id, body); return buildAndValidateResponse(artifact.attributes.identifier, body); }) 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 235e7152b83cf..4b2eb3ea1ddb0 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 @@ -18,6 +18,7 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; +import { AgentService } from '../../../../../ingest_manager/server'; import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; import { findAllUnenrolledAgentIds } from './support/unenroll'; @@ -26,8 +27,9 @@ interface HitSource { } interface MetadataRequestContext { + agentService: AgentService; + logger: Logger; requestHandlerContext: RequestHandlerContext; - endpointAppContext: EndpointAppContext; } const HOST_STATUS_MAPPING = new Map([ @@ -35,8 +37,12 @@ const HOST_STATUS_MAPPING = new Map([ ['offline', HostStatus.OFFLINE], ]); +const getLogger = (endpointAppContext: EndpointAppContext): Logger => { + return endpointAppContext.logFactory.get('metadata'); +}; + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { - const logger = endpointAppContext.logFactory.get('metadata'); + const logger = getLogger(endpointAppContext); router.post( { path: '/api/endpoint/metadata', @@ -66,12 +72,23 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }) ), }, - options: { authRequired: true }, + options: { authRequired: true, tags: ['access:securitySolution'] }, }, async (context, req, res) => { try { + const agentService = endpointAppContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } + + const metadataRequestContext: MetadataRequestContext = { + agentService, + logger, + requestHandlerContext: context, + }; + const unenrolledAgentIds = await findAllUnenrolledAgentIds( - endpointAppContext.service.getAgentService(), + agentService, context.core.savedObjects.client ); @@ -88,11 +105,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp 'search', queryParams )) as SearchResponse; + return res.ok({ - body: await mapToHostResultList(queryParams, response, { - endpointAppContext, - requestHandlerContext: context, - }), + body: await mapToHostResultList(queryParams, response, metadataRequestContext), }); } catch (err) { logger.warn(JSON.stringify(err, null, 2)); @@ -107,17 +122,22 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp validate: { params: schema.object({ id: schema.string() }), }, - options: { authRequired: true }, + options: { authRequired: true, tags: ['access:securitySolution'] }, }, async (context, req, res) => { + const agentService = endpointAppContext.service.getAgentService(); + if (agentService === undefined) { + return res.internalError({ body: 'agentService not available' }); + } + + const metadataRequestContext: MetadataRequestContext = { + agentService, + logger, + requestHandlerContext: context, + }; + try { - const doc = await getHostData( - { - endpointAppContext, - requestHandlerContext: context, - }, - req.params.id - ); + const doc = await getHostData(metadataRequestContext, req.params.id); if (doc) { return res.ok({ body: doc }); } @@ -164,17 +184,16 @@ async function findAgent( metadataRequestContext: MetadataRequestContext, hostMetadata: HostMetadata ): Promise { - const logger = metadataRequestContext.endpointAppContext.logFactory.get('metadata'); try { - return await metadataRequestContext.endpointAppContext.service - .getAgentService() - .getAgent( - metadataRequestContext.requestHandlerContext.core.savedObjects.client, - hostMetadata.elastic.agent.id - ); + return await metadataRequestContext.agentService.getAgent( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + hostMetadata.elastic.agent.id + ); } catch (e) { if (e.isBoom && e.output.statusCode === 404) { - logger.warn(`agent with id ${hostMetadata.elastic.agent.id} not found`); + metadataRequestContext.logger.warn( + `agent with id ${hostMetadata.elastic.agent.id} not found` + ); return undefined; } else { throw e; @@ -217,7 +236,7 @@ async function enrichHostMetadata( ): Promise { let hostStatus = HostStatus.ERROR; let elasticAgentId = hostMetadata?.elastic?.agent?.id; - const log = logger(metadataRequestContext.endpointAppContext); + const log = metadataRequestContext.logger; try { /** * Get agent status by elastic agent id if available or use the host id. @@ -228,12 +247,10 @@ async function enrichHostMetadata( log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); } - const status = await metadataRequestContext.endpointAppContext.service - .getAgentService() - .getAgentStatusById( - metadataRequestContext.requestHandlerContext.core.savedObjects.client, - elasticAgentId - ); + const status = await metadataRequestContext.agentService.getAgentStatusById( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + elasticAgentId + ); hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR; } catch (e) { if (e.isBoom && e.output.statusCode === 404) { @@ -248,7 +265,3 @@ async function enrichHostMetadata( host_status: hostStatus, }; } - -const logger = (endpointAppContext: EndpointAppContext): Logger => { - return endpointAppContext.logFactory.get('metadata'); -}; 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 42cce382ec20c..81027b42eb64f 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 @@ -47,8 +47,9 @@ describe('test endpoint route', () => { let routeHandler: RequestHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any let routeConfig: RouteConfig; - let mockAgentService: ReturnType< - typeof createMockEndpointAppContextServiceStartContract + // tests assume that ingestManager is enabled, and thus agentService is available + let mockAgentService: Required< + ReturnType >['agentService']; let endpointAppContextService: EndpointAppContextService; const noUnenrolledAgent = { @@ -70,7 +71,7 @@ describe('test endpoint route', () => { endpointAppContextService = new EndpointAppContextService(); const startContract = createMockEndpointAppContextServiceStartContract(); endpointAppContextService.start(startContract); - mockAgentService = startContract.agentService; + mockAgentService = startContract.agentService!; registerEndpointRoutes(routerMock, { logFactory: loggingSystemMock.create(), @@ -97,7 +98,7 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; expect(endpointResultList.hosts.length).toEqual(1); @@ -139,7 +140,7 @@ describe('test endpoint route', () => { expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ match_all: {}, }); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; expect(endpointResultList.hosts.length).toEqual(1); @@ -202,7 +203,7 @@ describe('test endpoint route', () => { ], }, }); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; expect(endpointResultList.hosts.length).toEqual(1); @@ -234,7 +235,10 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ + authRequired: true, + tags: ['access:securitySolution'], + }); expect(mockResponse.notFound).toBeCalled(); const message = mockResponse.notFound.mock.calls[0][0]?.body; expect(message).toEqual('Endpoint Not Found'); @@ -263,7 +267,10 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ + authRequired: true, + tags: ['access:securitySolution'], + }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result).toHaveProperty('metadata.Endpoint'); @@ -298,7 +305,10 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ + authRequired: true, + tags: ['access:securitySolution'], + }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); @@ -328,7 +338,10 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true }); + expect(routeConfig.options).toEqual({ + authRequired: true, + tags: ['access:securitySolution'], + }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts index 3c066e150288a..d5a30951e9398 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts @@ -6,14 +6,19 @@ import * as t from 'io-ts'; -export const body = t.string; +export const buffer = new t.Type( + 'buffer', + (input: unknown): input is Buffer => Buffer.isBuffer(input), + (input, context) => (Buffer.isBuffer(input) ? t.success(input) : t.failure(input, context)), + t.identity +); -export const created = t.number; // TODO: Make this into an ISO Date string check +export const created = t.number; export const encoding = t.keyof({ - 'application/json': null, + identity: null, }); export const schemaVersion = t.keyof({ - '1.0.0': null, + v1: null, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts index 537f7707889e4..3705062449c60 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts @@ -5,9 +5,8 @@ */ import * as t from 'io-ts'; -import { encoding } from '../common'; +import { buffer, encoding } from '../common'; -const body = t.string; const headers = t.exact( t.type({ 'content-encoding': encoding, @@ -17,7 +16,7 @@ const headers = t.exact( export const downloadArtifactResponseSchema = t.exact( t.type({ - body, + body: buffer, headers, }) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts index e4cd7f48a2901..aa11f4409269a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts @@ -12,7 +12,9 @@ import { sha256, size, } from '../../../../common/endpoint/schema/common'; -import { body, created } from './common'; +import { created } from './common'; + +export const body = t.string; // base64 export const internalArtifactSchema = t.exact( t.type({ diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index 08e29b5c6b82b..3e3b12c04d65c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -27,7 +27,7 @@ describe('artifact_client', () => { test('can create artifact', async () => { const savedObjectsClient = savedObjectsClientMock.create(); const artifactClient = getArtifactClientMock(savedObjectsClient); - const artifact = await getInternalArtifactMock('linux', '1.0.0'); + const artifact = await getInternalArtifactMock('linux', 'v1'); await artifactClient.createArtifact(artifact); expect(savedObjectsClient.create).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index e899905602c8d..ca53a891c4d6b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -16,7 +16,7 @@ export class ArtifactClient { } public getArtifactId(artifact: InternalArtifactSchema) { - return `${artifact.identifier}-${artifact.encodedSha256}`; + return `${artifact.identifier}-${artifact.decodedSha256}`; } public async getArtifact(id: string): Promise> { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts index bfeacbcedf2cb..d869ed9493abc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts @@ -12,7 +12,7 @@ export const getManifestClientMock = ( savedObjectsClient?: SavedObjectsClientContract ): ManifestClient => { if (savedObjectsClient !== undefined) { - return new ManifestClient(savedObjectsClient, '1.0.0'); + return new ManifestClient(savedObjectsClient, 'v1'); } - return new ManifestClient(savedObjectsClientMock.create(), '1.0.0'); + return new ManifestClient(savedObjectsClientMock.create(), 'v1'); }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts index 5780c6279ee6a..fe3f193bc8ff5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts @@ -14,7 +14,7 @@ import { ManifestClient } from './manifest_client'; describe('manifest_client', () => { describe('ManifestClient sanity checks', () => { test('can create ManifestClient', () => { - const manifestClient = new ManifestClient(savedObjectsClientMock.create(), '1.0.0'); + const manifestClient = new ManifestClient(savedObjectsClientMock.create(), 'v1'); expect(manifestClient).toBeInstanceOf(ManifestClient); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 483b3434d63f2..dfbe2572076d0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -15,6 +15,7 @@ import { buildArtifact, getFullEndpointExceptionList, } from '../../../lib/artifacts'; +import { ManifestConstants } from '../../../lib/artifacts/common'; import { InternalArtifactSchema } from '../../../schemas/artifacts'; import { getArtifactClientMock } from '../artifact_client.mock'; import { getManifestClientMock } from '../manifest_client.mock'; @@ -69,13 +70,13 @@ async function mockBuildExceptionListArtifacts( export class ManifestManagerMock extends ManifestManager { // @ts-ignore private buildExceptionListArtifacts = async () => { - return mockBuildExceptionListArtifacts('linux', '1.0.0'); + return mockBuildExceptionListArtifacts('linux', 'v1'); }; // @ts-ignore private getLastDispatchedManifest = jest .fn() - .mockResolvedValue(new Manifest(new Date(), '1.0.0', 'v0')); + .mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION)); // @ts-ignore private getManifestClient = jest diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 1d6dffadde61a..b1cbc41459f15 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { inflateSync } from 'zlib'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { ArtifactConstants, @@ -15,32 +16,33 @@ import { getPackageConfigServiceMock, getManifestManagerMock } from './manifest_ describe('manifest_manager', () => { describe('ManifestManager sanity checks', () => { - test('ManifestManager can refresh manifest', async () => { + test('ManifestManager can snapshot manifest', async () => { const manifestManager = getManifestManagerMock(); - const manifestWrapper = await manifestManager.refresh(); - expect(manifestWrapper!.diffs).toEqual([ + const snapshot = await manifestManager.getSnapshot(); + expect(snapshot!.diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-1.0.0-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', type: 'add', }, ]); - expect(manifestWrapper!.manifest).toBeInstanceOf(Manifest); + expect(snapshot!.manifest).toBeInstanceOf(Manifest); }); test('ManifestManager populates cache properly', async () => { const cache = new ExceptionsCache(5); const manifestManager = getManifestManagerMock({ cache }); - const manifestWrapper = await manifestManager.refresh(); - expect(manifestWrapper!.diffs).toEqual([ + const snapshot = await manifestManager.getSnapshot(); + expect(snapshot!.diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-1.0.0-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', type: 'add', }, ]); - const diff = manifestWrapper!.diffs[0]; - const entry = JSON.parse(cache.get(diff!.id)!); + await manifestManager.syncArtifacts(snapshot!, 'add'); + const diff = snapshot!.diffs[0]; + const entry = JSON.parse(inflateSync(cache.get(diff!.id)! as Buffer).toString()); expect(entry).toEqual({ entries: [ { @@ -73,16 +75,16 @@ describe('manifest_manager', () => { test('ManifestManager can dispatch manifest', async () => { const packageConfigService = getPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); - const manifestWrapperRefresh = await manifestManager.refresh(); - const manifestWrapperDispatch = await manifestManager.dispatch(manifestWrapperRefresh); - expect(manifestWrapperRefresh).toEqual(manifestWrapperDispatch); - const entries = manifestWrapperDispatch!.manifest.getEntries(); + const snapshot = await manifestManager.getSnapshot(); + const dispatched = await manifestManager.dispatch(snapshot!.manifest); + expect(dispatched).toEqual(true); + const entries = snapshot!.manifest.getEntries(); const artifact = Object.values(entries)[0].getArtifact(); expect( packageConfigService.update.mock.calls[0][2].inputs[0].config.artifact_manifest.value ).toEqual({ - manifest_version: 'v0', - schema_version: '1.0.0', + manifest_version: ManifestConstants.INITIAL_VERSION, + schema_version: 'v1', artifacts: { [artifact.identifier]: { compression_algorithm: 'none', @@ -91,7 +93,7 @@ describe('manifest_manager', () => { encoded_sha256: artifact.encodedSha256, decoded_size: artifact.decodedSize, encoded_size: artifact.encodedSize, - relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.encodedSha256}`, + relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.decodedSha256}`, }, }, }); @@ -103,15 +105,21 @@ describe('manifest_manager', () => { savedObjectsClient, }); - const manifestWrapperRefresh = await manifestManager.refresh(); - const manifestWrapperDispatch = await manifestManager.dispatch(manifestWrapperRefresh); + const snapshot = await manifestManager.getSnapshot(); + await manifestManager.syncArtifacts(snapshot!, 'add'); + const diff = { id: 'abcd', type: 'delete', }; - manifestWrapperDispatch!.diffs.push(diff); + snapshot!.diffs.push(diff); + + const dispatched = await manifestManager.dispatch(snapshot!.manifest); + expect(dispatched).toEqual(true); + + await manifestManager.commit(snapshot!.manifest); - await manifestManager.commit(manifestWrapperDispatch); + await manifestManager.syncArtifacts(snapshot!, 'delete'); // created new artifact expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index f7bc711d4bd05..b9e289cee62af 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, SavedObjectsClientContract, SavedObject } from 'src/core/server'; +import { Logger, SavedObjectsClientContract } from 'src/core/server'; +import { createHash } from 'crypto'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { ExceptionListClient } from '../../../../../../lists/server'; import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; @@ -17,9 +18,10 @@ import { ExceptionsCache, ManifestDiff, } from '../../../lib/artifacts'; -import { InternalArtifactSchema, InternalManifestSchema } from '../../../schemas/artifacts'; +import { InternalArtifactSchema } from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; +import { compressExceptionList } from '../../../lib/artifacts/lists'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -30,11 +32,11 @@ export interface ManifestManagerContext { cache: ExceptionsCache; } -export interface ManifestRefreshOpts { +export interface ManifestSnapshotOpts { initialize?: boolean; } -export interface WrappedManifest { +export interface ManifestSnapshot { manifest: Manifest; diffs: ManifestDiff[]; } @@ -56,215 +58,264 @@ export class ManifestManager { this.cache = context.cache; } - private getManifestClient(schemaVersion: string): ManifestClient { + /** + * Gets a ManifestClient for the provided schemaVersion. + * + * @param schemaVersion + */ + private getManifestClient(schemaVersion: string) { return new ManifestClient(this.savedObjectsClient, schemaVersion as ManifestSchemaVersion); } - private async buildExceptionListArtifacts( - schemaVersion: string - ): Promise { - const artifacts: InternalArtifactSchema[] = []; + /** + * Builds an array of artifacts (one per supported OS) based on the current + * state of exception-list-agnostic SO's. + * + * @param schemaVersion + */ + private async buildExceptionListArtifacts(schemaVersion: string) { + return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce( + async (acc: Promise, os) => { + const exceptionList = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + schemaVersion + ); + const artifacts = await acc; + const artifact = await buildArtifact(exceptionList, os, schemaVersion); + artifacts.push(artifact); + return Promise.resolve(artifacts); + }, + Promise.resolve([]) + ); + } + + /** + * Returns the last dispatched manifest based on the current state of the + * user-artifact-manifest SO. + * + * @param schemaVersion + */ + private async getLastDispatchedManifest(schemaVersion: string) { + try { + const manifestClient = this.getManifestClient(schemaVersion); + const manifestSo = await manifestClient.getManifest(); - for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { - const exceptionList = await getFullEndpointExceptionList( - this.exceptionListClient, - os, - schemaVersion + if (manifestSo.version === undefined) { + throw new Error('No version returned for manifest.'); + } + + const manifest = new Manifest( + new Date(manifestSo.attributes.created), + schemaVersion, + manifestSo.version ); - const artifact = await buildArtifact(exceptionList, os, schemaVersion); - artifacts.push(artifact); + for (const id of manifestSo.attributes.ids) { + const artifactSo = await this.artifactClient.getArtifact(id); + manifest.addEntry(artifactSo.attributes); + } + return manifest; + } catch (err) { + if (err.output.statusCode !== 404) { + throw err; + } + return null; } - - return artifacts; } - private async getLastDispatchedManifest(schemaVersion: string): Promise { - return this.getManifestClient(schemaVersion) - .getManifest() - .then(async (manifestSo: SavedObject) => { - if (manifestSo.version === undefined) { - throw new Error('No version returned for manifest.'); - } - const manifest = new Manifest( - new Date(manifestSo.attributes.created), - schemaVersion, - manifestSo.version - ); - - for (const id of manifestSo.attributes.ids) { - const artifactSo = await this.artifactClient.getArtifact(id); - manifest.addEntry(artifactSo.attributes); - } - - return manifest; - }) - .catch((err) => { - if (err.output.statusCode !== 404) { - throw err; - } + /** + * Snapshots a manifest based on current state of exception-list-agnostic SOs. + * + * @param opts TODO + */ + public async getSnapshot(opts?: ManifestSnapshotOpts) { + try { + let oldManifest: Manifest | null; + + // Get the last-dispatched manifest + oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION); + + if (oldManifest === null && opts !== undefined && opts.initialize) { + oldManifest = new Manifest( + new Date(), + ManifestConstants.SCHEMA_VERSION, + ManifestConstants.INITIAL_VERSION + ); // create empty manifest + } else if (oldManifest == null) { + this.logger.debug('Manifest does not exist yet. Waiting...'); return null; - }); - } + } + + // Build new exception list artifacts + const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); - public async refresh(opts?: ManifestRefreshOpts): Promise { - let oldManifest: Manifest | null; + // Build new manifest + const newManifest = Manifest.fromArtifacts( + artifacts, + ManifestConstants.SCHEMA_VERSION, + oldManifest + ); - // Get the last-dispatched manifest - oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION); + // Get diffs + const diffs = newManifest.diff(oldManifest); - if (oldManifest === null && opts !== undefined && opts.initialize) { - oldManifest = new Manifest(new Date(), ManifestConstants.SCHEMA_VERSION, 'v0'); // create empty manifest - } else if (oldManifest == null) { - this.logger.debug('Manifest does not exist yet. Waiting...'); + return { + manifest: newManifest, + diffs, + }; + } catch (err) { + this.logger.error(err); return null; } + } - // Build new exception list artifacts - const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); - - // Build new manifest - const newManifest = Manifest.fromArtifacts( - artifacts, - ManifestConstants.SCHEMA_VERSION, - oldManifest.getVersion() - ); + /** + * Syncs artifacts based on provided snapshot. + * + * Creates artifacts that do not yet exist and cleans up old artifacts that have been + * superceded by this snapshot. + * + * Can be filtered to apply one or both operations. + * + * @param snapshot + * @param diffType + */ + public async syncArtifacts(snapshot: ManifestSnapshot, diffType?: 'add' | 'delete') { + const filteredDiffs = snapshot.diffs.reduce((diffs: ManifestDiff[], diff) => { + if (diff.type === diffType || diffType === undefined) { + diffs.push(diff); + } else if (!['add', 'delete'].includes(diff.type)) { + // TODO: replace with io-ts schema + throw new Error(`Unsupported diff type: ${diff.type}`); + } + return diffs; + }, []); + + const adds = filteredDiffs.filter((diff) => { + return diff.type === 'add'; + }); + + const deletes = filteredDiffs.filter((diff) => { + return diff.type === 'delete'; + }); + + for (const diff of adds) { + const artifact = snapshot.manifest.getArtifact(diff.id); + if (artifact === undefined) { + throw new Error( + `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.` + ); + } + const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64')); + artifact.body = compressedArtifact.toString('base64'); + artifact.encodedSize = compressedArtifact.byteLength; + artifact.compressionAlgorithm = 'zlib'; + artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex'); - // Get diffs - const diffs = newManifest.diff(oldManifest); - - // Create new artifacts - for (const diff of diffs) { - if (diff.type === 'add') { - const artifact = newManifest.getArtifact(diff.id); - try { - await this.artifactClient.createArtifact(artifact); - - // Cache the body of the artifact - this.cache.set(diff.id, Buffer.from(artifact.body, 'base64').toString()); - } catch (err) { - if (err.status === 409) { - // This artifact already existed... - this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); - } else { - throw err; - } + try { + await this.artifactClient.createArtifact(artifact); + } catch (err) { + if (err.status === 409) { + this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); + } else { + throw err; } } + // Cache the body of the artifact + this.cache.set(diff.id, Buffer.from(artifact.body, 'base64')); } - return { - manifest: newManifest, - diffs, - }; + for (const diff of deletes) { + await this.artifactClient.deleteArtifact(diff.id); + // TODO: should we delete the cache entry here? + this.logger.info(`Cleaned up artifact ${diff.id}`); + } } /** - * Dispatches the manifest by writing it to the endpoint packageConfig. + * Dispatches the manifest by writing it to the endpoint package config. * - * @return {WrappedManifest | null} WrappedManifest if all dispatched, else null */ - public async dispatch(wrappedManifest: WrappedManifest | null): Promise { - if (wrappedManifest === null) { - this.logger.debug('wrappedManifest was null, aborting dispatch'); - return null; - } - - function showDiffs(diffs: ManifestDiff[]) { - return diffs.map((diff) => { - const op = diff.type === 'add' ? '(+)' : '(-)'; - return `${op}${diff.id}`; + public async dispatch(manifest: Manifest) { + let paging = true; + let page = 1; + let success = true; + + while (paging) { + const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, { + page, + perPage: 20, + kuery: 'ingest-package-configs.package.name:endpoint', }); - } - if (wrappedManifest.diffs.length > 0) { - this.logger.info(`Dispatching new manifest with diffs: ${showDiffs(wrappedManifest.diffs)}`); - - let paging = true; - let page = 1; - let success = true; - - while (paging) { - const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, { - page, - perPage: 20, - kuery: 'ingest-package-configs.package.name:endpoint', - }); - - for (const packageConfig of items) { - const { id, revision, updated_at, updated_by, ...newPackageConfig } = packageConfig; - - if ( - newPackageConfig.inputs.length > 0 && - newPackageConfig.inputs[0].config !== undefined - ) { - const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? { - value: {}, - }; - artifactManifest.value = wrappedManifest.manifest.toEndpointFormat(); - newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest; - - await this.packageConfigService - .update(this.savedObjectsClient, id, newPackageConfig) - .then((response) => { - this.logger.debug(`Updated package config ${id}`); - }) - .catch((err) => { - success = false; - this.logger.debug(`Error updating package config ${id}`); - this.logger.error(err); - }); - } else { + for (const packageConfig of items) { + const { id, revision, updated_at, updated_by, ...newPackageConfig } = packageConfig; + if (newPackageConfig.inputs.length > 0 && newPackageConfig.inputs[0].config !== undefined) { + const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? { + value: {}, + }; + artifactManifest.value = manifest.toEndpointFormat(); + newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest; + + try { + await this.packageConfigService.update(this.savedObjectsClient, id, newPackageConfig); + this.logger.debug( + `Updated package config ${id} with manifest version ${manifest.getVersion()}` + ); + } catch (err) { success = false; - this.logger.debug(`Package config ${id} has no config.`); + this.logger.debug(`Error updating package config ${id}`); + this.logger.error(err); } + } else { + success = false; + this.logger.debug(`Package config ${id} has no config.`); } - - paging = page * items.length < total; - page++; } - return success ? wrappedManifest : null; - } else { - this.logger.debug('No manifest diffs [no-op]'); + paging = page * items.length < total; + page++; } - return null; + // TODO: revisit success logic + return success; } - public async commit(wrappedManifest: WrappedManifest | null) { - if (wrappedManifest === null) { - this.logger.debug('wrappedManifest was null, aborting commit'); - return; - } - - const manifestClient = this.getManifestClient(wrappedManifest.manifest.getSchemaVersion()); + /** + * Commits a manifest to indicate that it has been dispatched. + * + * @param manifest + */ + public async commit(manifest: Manifest) { + const manifestClient = this.getManifestClient(manifest.getSchemaVersion()); // Commit the new manifest - if (wrappedManifest.manifest.getVersion() === 'v0') { - await manifestClient.createManifest(wrappedManifest.manifest.toSavedObject()); + if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) { + await manifestClient.createManifest(manifest.toSavedObject()); } else { - const version = wrappedManifest.manifest.getVersion(); - if (version === 'v0') { + const version = manifest.getVersion(); + if (version === ManifestConstants.INITIAL_VERSION) { throw new Error('Updating existing manifest with baseline version. Bad state.'); } - await manifestClient.updateManifest(wrappedManifest.manifest.toSavedObject(), { + await manifestClient.updateManifest(manifest.toSavedObject(), { version, }); } - this.logger.info(`Commited manifest ${wrappedManifest.manifest.getVersion()}`); + this.logger.info(`Committed manifest ${manifest.getVersion()}`); + } - // Clean up old artifacts - for (const diff of wrappedManifest.diffs) { - try { - if (diff.type === 'delete') { - await this.artifactClient.deleteArtifact(diff.id); - this.logger.info(`Cleaned up artifact ${diff.id}`); - } - } catch (err) { - this.logger.error(err); - } - } + /** + * Confirms that a packageConfig exists with provided name. + */ + public async confirmPackageConfigExists(name: string) { + // TODO: what if there are multiple results? uh oh. + const { total } = await this.packageConfigService.list(this.savedObjectsClient, { + page: 1, + perPage: 20, + kuery: `ingest-package-configs.name:${name}`, + }); + return total > 0; } } 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 a9d07389797db..e46d3be44dbd1 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 @@ -84,6 +84,12 @@ export const timelineSchema = gql` kqlQuery: String queryMatch: QueryMatchInput and: [DataProviderInput!] + type: DataProviderType + } + + enum DataProviderType { + default + template } input KueryFilterQueryInput { @@ -194,6 +200,7 @@ export const timelineSchema = gql` excluded: Boolean kqlQuery: String queryMatch: QueryMatchResult + type: DataProviderType and: [DataProviderResult!] } diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index a702b1a72f0a9..52bb4a9862160 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -187,6 +187,8 @@ export interface DataProviderInput { queryMatch?: Maybe; and?: Maybe; + + type?: Maybe; } export interface QueryMatchInput { @@ -344,6 +346,11 @@ export enum TlsFields { _id = '_id', } +export enum DataProviderType { + default = 'default', + template = 'template', +} + export enum TimelineStatus { active = 'active', draft = 'draft', @@ -2032,6 +2039,8 @@ export interface DataProviderResult { queryMatch?: Maybe; + type?: Maybe; + and?: Maybe; } @@ -8368,6 +8377,8 @@ export namespace DataProviderResultResolvers { queryMatch?: QueryMatchResolver, TypeParent, TContext>; + type?: TypeResolver, TypeParent, TContext>; + and?: AndResolver, TypeParent, TContext>; } @@ -8401,6 +8412,11 @@ export namespace DataProviderResultResolvers { Parent = DataProviderResult, TContext = SiemContext > = Resolver; + export type TypeResolver< + R = Maybe, + Parent = DataProviderResult, + TContext = SiemContext + > = Resolver; export type AndResolver< R = Maybe, Parent = DataProviderResult, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts index 68e7f8d5e6fe1..eb8f6f5022985 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts @@ -35,6 +35,7 @@ export const pickSavedTimeline = ( if (savedTimeline.timelineType === TimelineType.default) { savedTimeline.timelineType = savedTimeline.timelineType ?? TimelineType.default; + savedTimeline.status = savedTimeline.status ?? TimelineStatus.active; savedTimeline.templateTimelineId = null; savedTimeline.templateTimelineVersion = null; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts index f5345c3dce222..0286ef558810e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts @@ -28,7 +28,7 @@ import { import { CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, CREATE_TIMELINE_ERROR_MESSAGE, -} from './utils/create_timelines'; +} from './utils/failure_cases'; describe('create timelines', () => { let server: ReturnType; @@ -167,8 +167,8 @@ describe('create timelines', () => { }); }); - describe('Manipulate template timeline', () => { - describe('Create a new template timeline', () => { + describe('Manipulate timeline template', () => { + describe('Create a new timeline template', () => { beforeEach(async () => { jest.doMock('../saved_object', () => { return { @@ -199,19 +199,19 @@ describe('create timelines', () => { await server.inject(mockRequest, context); }); - test('should Create a new template timeline savedObject', async () => { + test('should Create a new timeline template savedObject', async () => { expect(mockPersistTimeline).toHaveBeenCalled(); }); - test('should Create a new template timeline savedObject without timelineId', async () => { + test('should Create a new timeline template savedObject without timelineId', async () => { expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); }); - test('should Create a new template timeline savedObject without template timeline version', async () => { + test('should Create a new timeline template savedObject without timeline template version', async () => { expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); }); - test('should Create a new template timeline savedObject witn given template timeline', async () => { + test('should Create a new timeline template savedObject witn given timeline template', async () => { expect(mockPersistTimeline.mock.calls[0][3]).toEqual( createTemplateTimelineWithTimelineId.timeline ); @@ -234,7 +234,7 @@ describe('create timelines', () => { }); }); - describe('Create a template timeline already exist', () => { + describe('Create a timeline template already exist', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { 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 15fb8f3411cfa..248bf358064c0 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 @@ -409,7 +409,7 @@ describe('import timelines', () => { }); }); -describe('import template timelines', () => { +describe('import timeline templates', () => { let server: ReturnType; let request: ReturnType; let securitySetup: SecurityPluginSetup; @@ -473,7 +473,7 @@ describe('import template timelines', () => { })); }); - describe('Import a new template timeline', () => { + describe('Import a new timeline template', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { @@ -596,7 +596,7 @@ describe('import template timelines', () => { }); }); - describe('Import a template timeline already exist', () => { + describe('Import a timeline template already exist', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { @@ -704,7 +704,7 @@ describe('import template timelines', () => { expect(response.status).toEqual(200); }); - test('should throw error if with given template timeline version conflict', async () => { + test('should throw error if with given timeline template version conflict', async () => { mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([ mockDuplicateIdErrors, [ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts index fb4991d7d1e7d..56e4e81b4214b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts @@ -46,7 +46,7 @@ import { createTimelines } from './utils/create_timelines'; import { TimelineStatus } from '../../../../common/types/timeline'; const CHUNK_PARSED_OBJECT_SIZE = 10; -const DEFAULT_IMPORT_ERROR = `Something went wrong, there's something we didn't handle properly, please help us improve by providing the file you try to import on https://discuss.elastic.co/c/security/siem`; +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 importTimelinesRoute = ( router: IRouter, @@ -158,7 +158,7 @@ export const importTimelinesRoute = ( await compareTimelinesStatus.init(); const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; if (compareTimelinesStatus.isCreatableViaImport) { - // create timeline / template timeline + // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, timeline: { @@ -199,7 +199,7 @@ export const importTimelinesRoute = ( ); } else { if (compareTimelinesStatus.isUpdatableViaImport) { - // update template timeline + // update timeline template newTimeline = await createTimelines({ frameworkRequest, timeline: parsedTimelineObject, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts index 3cedb925649a2..17e6e8a84ef22 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts @@ -168,8 +168,8 @@ describe('update timelines', () => { }); }); - describe('Manipulate template timeline', () => { - describe('Update an existing template timeline', () => { + describe('Manipulate timeline template', () => { + describe('Update an existing timeline template', () => { beforeEach(async () => { jest.doMock('../saved_object', () => { return { @@ -209,25 +209,25 @@ describe('update timelines', () => { ); }); - test('should Update existing template timeline with template timelineId', async () => { + test('should Update existing timeline template with timeline templateId', async () => { expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( updateTemplateTimelineWithTimelineId.timeline.templateTimelineId ); }); - test('should Update existing template timeline with timelineId', async () => { + test('should Update existing timeline template with timelineId', async () => { expect(mockPersistTimeline.mock.calls[0][1]).toEqual( updateTemplateTimelineWithTimelineId.timelineId ); }); - test('should Update existing template timeline with timeline version', async () => { + test('should Update existing timeline template with timeline version', async () => { expect(mockPersistTimeline.mock.calls[0][2]).toEqual( updateTemplateTimelineWithTimelineId.version ); }); - test('should Update existing template timeline witn given timeline', async () => { + test('should Update existing timeline template witn given timeline', async () => { expect(mockPersistTimeline.mock.calls[0][3]).toEqual( updateTemplateTimelineWithTimelineId.timeline ); @@ -241,7 +241,7 @@ describe('update timelines', () => { expect(mockPersistNote).not.toBeCalled(); }); - test('returns 200 when create template timeline successfully', async () => { + test('returns 200 when create timeline template successfully', async () => { const response = await server.inject( getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), context @@ -250,7 +250,7 @@ describe('update timelines', () => { }); }); - describe("Update a template timeline that doesn't exist", () => { + describe("Update a timeline template that doesn't exist", () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts index a6d379e534bc2..6e3e3a420963f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts @@ -179,8 +179,8 @@ describe('CompareTimelinesStatus', () => { }); }); - describe('template timeline', () => { - describe('given template timeline exists', () => { + describe('timeline template', () => { + describe('given timeline template exists', () => { const mockGetTimeline: jest.Mock = jest.fn(); const mockGetTemplateTimeline: jest.Mock = jest.fn(); @@ -249,12 +249,12 @@ describe('CompareTimelinesStatus', () => { expect(timelineObj.isUpdatableViaImport).toEqual(true); }); - test('should indicate we are handling a template timeline', () => { + test('should indicate we are handling a timeline template', () => { expect(timelineObj.isHandlingTemplateTimeline).toEqual(true); }); }); - describe('given template timeline does NOT exists', () => { + describe('given timeline template does NOT exists', () => { const mockGetTimeline: jest.Mock = jest.fn(); const mockGetTemplateTimeline: jest.Mock = jest.fn(); @@ -339,7 +339,7 @@ describe('CompareTimelinesStatus', () => { expect(error?.body).toEqual(UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE); }); - test('should indicate we are handling a template timeline', () => { + test('should indicate we are handling a timeline template', () => { expect(timelineObj.isHandlingTemplateTimeline).toEqual(true); }); }); @@ -427,7 +427,7 @@ describe('CompareTimelinesStatus', () => { }); }); - describe('template timeline', () => { + describe('timeline template', () => { const mockGetTimeline: jest.Mock = jest.fn(); const mockGetTemplateTimeline: jest.Mock = jest.fn(); let timelineObj: TimelinesStatusType; @@ -589,7 +589,7 @@ describe('CompareTimelinesStatus', () => { }); }); - describe('immutable template timeline', () => { + describe('immutable timeline template', () => { const mockGetTimeline: jest.Mock = jest.fn(); const mockGetTemplateTimeline: jest.Mock = jest.fn(); let timelineObj: TimelinesStatusType; @@ -662,7 +662,7 @@ describe('CompareTimelinesStatus', () => { }); }); - describe('If create template timeline without template timeline id', () => { + describe('If create timeline template without timeline template id', () => { const mockGetTimeline: jest.Mock = jest.fn(); const mockGetTemplateTimeline: jest.Mock = jest.fn(); @@ -724,7 +724,7 @@ describe('CompareTimelinesStatus', () => { }); }); - describe('Throw error if template timeline version is conflict when update via import', () => { + describe('Throw error if timeline template version is conflict when update via import', () => { const mockGetTimeline: jest.Mock = jest.fn(); const mockGetTemplateTimeline: jest.Mock = jest.fn(); 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 abe298566341c..67965469e1a9f 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 @@ -13,11 +13,6 @@ import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/ import { SavedNote } from '../../../../../common/types/timeline/note'; import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; -export const CREATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE timeline with POST is not allowed, please use PATCH instead'; -export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; - export const saveTimelines = ( frameworkRequest: FrameworkRequest, timeline: SavedTimeline, 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 60ba5389280c4..d41e8fc190983 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 @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { isEmpty } from 'lodash/fp'; import { TimelineSavedObject, @@ -11,27 +12,31 @@ import { } from '../../../../../common/types/timeline'; export const UPDATE_TIMELINE_ERROR_MESSAGE = - 'CREATE timeline with PATCH is not allowed, please use POST instead'; + 'You cannot create new timelines with PATCH. Use POST instead.'; export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = - "CREATE template timeline with PATCH is not allowed, please use POST instead (Given template timeline doesn't exist)"; + 'You cannot create new Timeline templates with PATCH. Use POST instead (templateTimelineId does not exist).'; export const NO_MATCH_VERSION_ERROR_MESSAGE = - 'TimelineVersion conflict: The given version doesn not match with existing timeline'; + 'Timeline template version conflict. The provided templateTimelineVersion does not match the current template.'; export const NO_MATCH_ID_ERROR_MESSAGE = - "Timeline id doesn't match with existing template timeline"; -export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict'; + 'There are no Timeline templates that match the provided templateTimelineId.'; +export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = + 'To update existing Timeline templates, you must increment the templateTimelineVersion value.'; export const CREATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE timeline with POST is not allowed, please use PATCH instead'; + 'You cannot update timelines with POST. Use PATCH instead.'; export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; -export const EMPTY_TITLE_ERROR_MESSAGE = 'Title cannot be empty'; -export const UPDATE_STATUS_ERROR_MESSAGE = 'Update an immutable timeline is is not allowed'; + 'You cannot update Timeline templates with POST. Use PATCH instead.'; +export const EMPTY_TITLE_ERROR_MESSAGE = 'The title field cannot be empty.'; +export const UPDATE_STATUS_ERROR_MESSAGE = + 'You are not allowed to set the status field value to immutable.'; export const CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE = - 'Create template timeline without a valid templateTimelineVersion is not allowed. Please start from 1 to create a new template timeline'; -export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = 'Cannot create a draft timeline'; -export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'Update status is not allowed'; -export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'Update timelineType is not allowed'; + 'You must provide a valid templateTimelineVersion value. Use 1 for new Timeline templates.'; +export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = + 'You are not allowed to set the status field value to draft.'; +export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'You are not allowed to set the status field.'; +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 = - 'Update timeline via import is not allowed'; + 'You cannot update a timeline via imports. Use the UI to modify existing timelines.'; const isUpdatingStatus = ( isHandlingTemplateTimeline: boolean, @@ -81,8 +86,8 @@ const commonUpdateTemplateTimelineCheck = ( } if (existTemplateTimeline == null && templateTimelineVersion != null) { - // template timeline !exists - // Throw error to create template timeline in patch + // timeline template !exists + // Throw error to create timeline template in patch return { body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, statusCode: 405, @@ -94,7 +99,7 @@ const commonUpdateTemplateTimelineCheck = ( existTemplateTimeline != null && existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId ) { - // Throw error you can not have a no matching between your timeline and your template timeline during an update + // Throw error you can not have a no matching between your timeline and your timeline template during an update return { body: NO_MATCH_ID_ERROR_MESSAGE, statusCode: 409, @@ -191,7 +196,7 @@ const createTemplateTimelineCheck = ( existTemplateTimeline: TimelineSavedObject | null ) => { if (isHandlingTemplateTimeline && existTemplateTimeline != null) { - // Throw error to create template timeline in patch + // Throw error to create timeline template in patch return { body: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, statusCode: 405, @@ -264,7 +269,7 @@ export const checkIsUpdateViaImportFailureCases = ( existTemplateTimeline.templateTimelineVersion != null && existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion ) { - // Throw error you can not update a template timeline version with an old version + // Throw error you can not update a timeline template version with an old version return { body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE, statusCode: 409, @@ -365,7 +370,7 @@ export const checkIsCreateViaImportFailureCases = ( } } else { if (existTemplateTimeline != null) { - // Throw error to create template timeline in patch + // Throw error to create timeline template in patch return { body: getImportExistingTimelineError(existTemplateTimeline.savedObjectId), statusCode: 405, 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 ec90fc6d8e071..f4dbd2db3329c 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 @@ -7,11 +7,7 @@ import { getOr } from 'lodash/fp'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; -import { - UNAUTHENTICATED_USER, - disableTemplate, - enableElasticFilter, -} from '../../../common/constants'; +import { UNAUTHENTICATED_USER, enableElasticFilter } from '../../../common/constants'; import { NoteSavedObject } from '../../../common/types/timeline/note'; import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event'; import { @@ -158,10 +154,9 @@ const getTimelineTypeFilter = ( ? `siem-ui-timeline.attributes.createdBy: "Elsatic"` : `not siem-ui-timeline.attributes.createdBy: "Elastic"`; - const filters = - !disableTemplate && enableElasticFilter - ? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter] - : [typeFilter, draftFilter, immutableFilter]; + const filters = enableElasticFilter + ? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter] + : [typeFilter, draftFilter, immutableFilter]; return filters.filter((f) => f != null).join(' and '); }; @@ -183,16 +178,7 @@ export const getAllTimeline = async ( searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - /** - * CreateTemplateTimelineBtn - * Remove the comment here to enable template timeline and apply the change below - * filter: getTimelineTypeFilter(timelineType, templateTimelineType, false) - */ - filter: getTimelineTypeFilter( - disableTemplate ? TimelineType.default : timelineType, - disableTemplate ? null : templateTimelineType, - disableTemplate ? null : status - ), + filter: getTimelineTypeFilter(timelineType, templateTimelineType, status), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts index 51bff033b8791..22b98930f3181 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts @@ -64,6 +64,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { kqlQuery: { type: 'text', }, + type: { + type: 'text', + }, queryMatch: { properties: { field: { @@ -100,6 +103,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { kqlQuery: { type: 'text', }, + type: { + type: 'text', + }, queryMatch: { properties: { field: { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 568aa0e85de93..d4935f1aabc1c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -24,7 +24,7 @@ import { ListPluginSetup } from '../../lists/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; import { LicensingPluginSetup } from '../../licensing/server'; -import { IngestManagerStartContract } from '../../ingest_manager/server'; +import { IngestManagerStartContract, ExternalCallback } from '../../ingest_manager/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -54,14 +54,14 @@ export interface SetupPlugins { licensing: LicensingPluginSetup; security?: SecuritySetup; spaces?: SpacesSetup; - taskManager: TaskManagerSetupContract; + taskManager?: TaskManagerSetupContract; ml?: MlSetup; lists?: ListPluginSetup; } export interface StartPlugins { - ingestManager: IngestManagerStartContract; - taskManager: TaskManagerStartContract; + ingestManager?: IngestManagerStartContract; + taskManager?: TaskManagerStartContract; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -72,7 +72,7 @@ export interface PluginStart {} const securitySubPlugins = [ APP_ID, `${APP_ID}:${SecurityPageName.overview}`, - `${APP_ID}:${SecurityPageName.alerts}`, + `${APP_ID}:${SecurityPageName.detections}`, `${APP_ID}:${SecurityPageName.hosts}`, `${APP_ID}:${SecurityPageName.network}`, `${APP_ID}:${SecurityPageName.timelines}`, @@ -227,11 +227,15 @@ export class Plugin implements IPlugin { + return plugins.taskManager && plugins.lists; + }; + + if (exceptionListsSetupEnabled()) { this.lists = plugins.lists; this.manifestTask = new ManifestTask({ endpointAppContext: endpointContext, - taskManager: plugins.taskManager, + taskManager: plugins.taskManager!, }); } @@ -245,32 +249,41 @@ export class Plugin implements IPlugin void) | undefined; + + const exceptionListsStartEnabled = () => { + return this.lists && plugins.taskManager && plugins.ingestManager; + }; + + if (exceptionListsStartEnabled()) { + const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana'); const artifactClient = new ArtifactClient(savedObjectsClient); + + registerIngestCallback = plugins.ingestManager!.registerExternalCallback; manifestManager = new ManifestManager({ savedObjectsClient, artifactClient, exceptionListClient, - packageConfigService: plugins.ingestManager.packageConfigService, + packageConfigService: plugins.ingestManager!.packageConfigService, logger: this.logger, cache: this.exceptionsCache, }); } this.endpointAppContextService.start({ - agentService: plugins.ingestManager.agentService, + agentService: plugins.ingestManager?.agentService, + logger: this.logger, manifestManager, - registerIngestCallback: plugins.ingestManager.registerExternalCallback, + registerIngestCallback, savedObjectsStart: core.savedObjects, }); - if (this.manifestTask) { + if (exceptionListsStartEnabled() && this.manifestTask) { this.manifestTask.start({ - taskManager: plugins.taskManager, + taskManager: plugins.taskManager!, }); } else { - this.logger.debug('Manifest task not available.'); + this.logger.debug('User artifacts task not available.'); } return {}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 72d21400540fd..92285d8bf72f8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -87,6 +87,7 @@ "advancedSettings.categoryNames.notificationsLabel": "通知", "advancedSettings.categoryNames.reportingLabel": "レポート", "advancedSettings.categoryNames.searchLabel": "検索", + "advancedSettings.categoryNames.securitySolutionLabel": "Security Solution", "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可視化", "advancedSettings.categorySearchLabel": "カテゴリー", @@ -123,119 +124,6 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", - "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", - "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", - "apmOss.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました", - "apmOss.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。", - "apmOss.tutorial.apmAgents.statusCheck.title": "エージェントステータス", - "apmOss.tutorial.apmAgents.title": "APM エージェント", - "apmOss.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。", - "apmOss.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中", - "apmOss.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認", - "apmOss.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。", - "apmOss.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました", - "apmOss.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。", - "apmOss.tutorial.apmServer.statusCheck.title": "APM Server ステータス", - "apmOss.tutorial.apmServer.title": "APM Server", - "apmOss.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します", - "apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。", - "apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", - "apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", - "apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", - "apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", - "apmOss.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", - "apmOss.tutorial.djangoClient.configure.title": "エージェントの構成", - "apmOss.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", - "apmOss.tutorial.djangoClient.install.title": "APM エージェントのインストール", - "apmOss.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、(例: 非 ASP.NET Core アプリケーションの場合)、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:", - "apmOss.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例: 「appsettings.json」ファイル) から構成を読み込みます。", - "apmOss.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。", - "apmOss.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加", - "apmOss.tutorial.dotNetClient.download.textPre": "[NuGet]({allNuGetPackagesLink}) から .NET アプリケーションにエージェントパッケージを追加してください。用途の異なる複数の NuGet パッケージがあります。\n\nEntity Framework Core の ASP.NET Core アプリケーションの場合は、[Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) パッケージをダウンロードしてください。このパッケージは、自動的にすべてのエージェントコンポーネントをアプリケーションに追加します。\n\n 依存性を最低限に抑えたい場合、ASP.NET Core の監視のみに [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) パッケージ、または Entity Framework Core の監視のみに [Elastic.Apm.EfCore]({efCorePackageLink}) パッケージを使用することができます。\n\n 手動インストルメンテーションのみにパブリック Agent API を使用する場合は、[Elastic.Apm]({elasticApmPackageLink}) パッケージを使用してください。", - "apmOss.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード", - "apmOss.tutorial.downloadServer.title": "APM Server をダウンロードして展開します", - "apmOss.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", - "apmOss.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", - "apmOss.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。", - "apmOss.tutorial.editConfig.title": "構成を編集する", - "apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", - "apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。", - "apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します", - "apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", - "apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", - "apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", - "apmOss.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", - "apmOss.tutorial.flaskClient.configure.title": "エージェントの構成", - "apmOss.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", - "apmOss.tutorial.flaskClient.install.title": "APM エージェントのインストール", - "apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:", - "apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", - "apmOss.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。", - "apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行可能な名前が使用されます。", - "apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server にトークンが必要な場合に使います", - "apmOss.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。", - "apmOss.tutorial.goClient.configure.title": "エージェントの構成", - "apmOss.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。", - "apmOss.tutorial.goClient.install.title": "APM エージェントのインストール", - "apmOss.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。", - "apmOss.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション", - "apmOss.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。", - "apmOss.tutorial.javaClient.download.textPre": "[Maven Central]({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。", - "apmOss.tutorial.javaClient.download.title": "APM エージェントのダウンロード", - "apmOss.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加してエージェントをシステムプロパティで構成します。\n\n * 必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)\n * カスタム APM Server URL (デフォルト: {customApmServerUrl})\n * アプリケーションのベースパッケージを設定します", - "apmOss.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動", - "apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション]({documentationLink}) をご覧ください。", - "apmOss.tutorial.jsClient.enableRealUserMonitoring.title": "APMサーバーのリアルユーザー監視サポートを有効にする", - "apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", - "apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)", - "apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要)", - "apmOss.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント]({docLink}) をご覧ください。", - "apmOss.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。", - "apmOss.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ", - "apmOss.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加