diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 445cc0e51073f..1c59d6d9aaaf8 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.16.0 +ARG NODE_VERSION=14.16.1 FROM node:${NODE_VERSION} AS base diff --git a/.eslintignore b/.eslintignore index bbd8e3f88a378..4058d971b7642 100644 --- a/.eslintignore +++ b/.eslintignore @@ -21,19 +21,13 @@ snapshots.js # plugin overrides /src/core/lib/kbn_internal_native_observable -/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/plugins/vis_type_timelion/common/_generated_/** -/x-pack/legacy/plugins/**/__tests__/fixtures/** /x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook/build /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** -/x-pack/legacy/plugins/infra/common/graphql/types.ts -/x-pack/legacy/plugins/infra/public/graphql/types.ts -/x-pack/legacy/plugins/infra/server/graphql/types.ts -/x-pack/legacy/plugins/maps/public/vendor/** # package overrides /packages/elastic-eslint-config-kibana diff --git a/.eslintrc.js b/.eslintrc.js index 65c8e8ee2e694..19ba7cacc3c44 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -410,11 +410,7 @@ module.exports = { errorMessage: `Common code can not import from server or public, use a common directory.`, }, { - target: [ - 'src/legacy/**/*', - '(src|x-pack)/plugins/**/(public|server)/**/*', - 'examples/**/*', - ], + target: ['(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*'], from: [ 'src/core/public/**/*', '!src/core/public/index.ts', // relative import @@ -428,8 +424,6 @@ module.exports = { '!src/core/server/mocks{,.ts}', '!src/core/server/types{,.ts}', '!src/core/server/test_utils{,.ts}', - '!src/core/server/utils', // ts alias - '!src/core/server/utils/**/*', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 '!src/core/server/*.test.mocks{,.ts}', @@ -442,7 +436,6 @@ module.exports = { }, { target: [ - 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', '!(src|x-pack)/**/*.test.*', @@ -482,7 +475,7 @@ module.exports = { }, { target: ['src/core/**/*'], - from: ['plugins/**/*', 'src/plugins/**/*', 'src/legacy/ui/**/*'], + from: ['plugins/**/*', 'src/plugins/**/*'], errorMessage: 'The core cannot depend on any plugins.', }, { @@ -490,19 +483,6 @@ module.exports = { from: ['ui/**/*'], errorMessage: 'Plugins cannot import legacy UI code.', }, - { - from: ['src/legacy/ui/**/*', 'ui/**/*'], - target: [ - 'test/plugin_functional/plugins/**/public/np_ready/**/*', - 'test/plugin_functional/plugins/**/server/np_ready/**/*', - ], - allowSameFolder: true, - errorMessage: - 'NP-ready code should not import from /src/legacy/ui/** folder. ' + - 'Instead of importing from /src/legacy/ui/** deeply within a np_ready folder, ' + - 'import those things once at the top level of your plugin and pass those down, just ' + - 'like you pass down `core` and `plugins` objects.', - }, ], }, ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d14556ea1dabf..92e39c2e634e5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -79,6 +79,7 @@ # Uptime /x-pack/plugins/uptime @elastic/uptime +/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime /x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime /x-pack/test/functional/apps/uptime @elastic/uptime /x-pack/test/api_integration/apis/uptime @elastic/uptime @@ -107,7 +108,6 @@ /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation -#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-presentation #CC# /x-pack/plugins/dashboard_mode @elastic/kibana-presentation @@ -146,7 +146,6 @@ /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis -#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis @@ -165,7 +164,6 @@ /packages/kbn-utils/ @elastic/kibana-operations /packages/kbn-cli-dev-mode/ @elastic/kibana-operations /src/cli/keystore/ @elastic/kibana-operations -/src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations @@ -202,9 +200,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/legacy/server/config/ @elastic/kibana-core -/src/legacy/server/http/ @elastic/kibana-core -/src/legacy/server/logging/ @elastic/kibana-core /src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core @@ -214,9 +209,6 @@ /src/plugins/kibana_overview/ @elastic/kibana-core /x-pack/plugins/global_search_bar/ @elastic/kibana-core #CC# /src/core/server/csp/ @elastic/kibana-core -#CC# /src/legacy/server/config/ @elastic/kibana-core -#CC# /src/legacy/server/http/ @elastic/kibana-core -#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-core #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index d9d2d6d1ddb8b..4966a0b506317 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,5 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} - - diff --git a/.node-version b/.node-version index 2a0dc9a810cf3..6b17d228d3351 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.16.0 +14.16.1 diff --git a/.nvmrc b/.nvmrc index 2a0dc9a810cf3..6b17d228d3351 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.16.0 +14.16.1 diff --git a/NOTICE.txt b/NOTICE.txt index 2341a478cbda9..4eec329b7a603 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -261,33 +261,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -This product bundles childnode-remove which is available under a -"MIT" license. - -The MIT License (MIT) - -Copyright (c) 2016-present, jszhou -https://github.com/jserz/js_piece - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - --- This product bundles code based on probot-metadata@1.0.0 which is available under a "MIT" license. diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 4639414b4564e..e74c646eedeaf 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.2.3") # we can update that rule. node_repositories( node_repositories = { - "14.16.0-darwin_amd64": ("node-v14.16.0-darwin-x64.tar.gz", "node-v14.16.0-darwin-x64", "14ec767e376d1e2e668f997065926c5c0086ec46516d1d45918af8ae05bd4583"), - "14.16.0-linux_arm64": ("node-v14.16.0-linux-arm64.tar.xz", "node-v14.16.0-linux-arm64", "440489a08bfd020e814c9e65017f58d692299ac3f150c8e78d01abb1104c878a"), - "14.16.0-linux_s390x": ("node-v14.16.0-linux-s390x.tar.xz", "node-v14.16.0-linux-s390x", "335348e46f45284b6356416ef58f85602d2dee99094588b65900f6c8839df77e"), - "14.16.0-linux_amd64": ("node-v14.16.0-linux-x64.tar.xz", "node-v14.16.0-linux-x64", "2e079cf638766fedd720d30ec8ffef5d6ceada4e8b441fc2a093cb9a865f4087"), - "14.16.0-windows_amd64": ("node-v14.16.0-win-x64.zip", "node-v14.16.0-win-x64", "716045c2f16ea10ca97bd04cf2e5ef865f9c4d6d677a9bc25e2ea522b594af4f"), + "14.16.1-darwin_amd64": ("node-v14.16.1-darwin-x64.tar.gz", "node-v14.16.1-darwin-x64", "b762b72fc149629b7e394ea9b75a093cad709a9f2f71480942945d8da0fc1218"), + "14.16.1-linux_arm64": ("node-v14.16.1-linux-arm64.tar.xz", "node-v14.16.1-linux-arm64", "b4d474e79f7d33b3b4430fad25c3f836b82ce2d5bb30d4a2c9fa20df027e40da"), + "14.16.1-linux_s390x": ("node-v14.16.1-linux-s390x.tar.xz", "node-v14.16.1-linux-s390x", "af9982fef32e4a3e4a5d66741dcf30ac9c27613bd73582fa1dae1fb25003047a"), + "14.16.1-linux_amd64": ("node-v14.16.1-linux-x64.tar.xz", "node-v14.16.1-linux-x64", "85a89d2f68855282c87851c882d4c4bbea4cd7f888f603722f0240a6e53d89df"), + "14.16.1-windows_amd64": ("node-v14.16.1-win-x64.zip", "node-v14.16.1-win-x64", "e469db37b4df74627842d809566c651042d86f0e6006688f0f5fe3532c6dfa41"), }, - node_version = "14.16.0", + node_version = "14.16.1", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/api_docs/data_ui.json b/api_docs/data_ui.json index 52d5100237394..63956cdcb3799 100644 --- a/api_docs/data_ui.json +++ b/api_docs/data_ui.json @@ -415,6 +415,34 @@ "signature": [ "string | undefined" ] + }, + { + "tags": [], + "id": "def-public.QueryStringInputProps.autoSubmit", + "type": "CompoundType", + "label": "autoSubmit", + "description": [], + "source": { + "path": "src/plugins/data/public/ui/query_string_input/query_string_input.tsx", + "lineNumber": 72 + }, + "signature": [ + "boolean | undefined" + ] + }, + { + "tags": [], + "id": "def-public.QueryStringInputProps.storageKey", + "type": "string", + "label": "storageKey", + "description": [], + "source": { + "path": "src/plugins/data/public/ui/query_string_input/query_string_input.tsx", + "lineNumber": 76 + }, + "signature": [ + "string | undefined" + ] } ], "source": { @@ -460,7 +488,7 @@ "section": "def-public.SearchBarProps", "text": "SearchBarProps" }, - ", \"filters\" | \"query\" | \"intl\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, \"filters\" | \"query\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, any> & { WrappedComponent: React.ComponentType, \"filters\" | \"query\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"storageKey\" | \"disableLanguageSwitcher\" | \"isInvalid\" | \"autoSubmit\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, any> & { WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }" + ", \"filters\" | \"query\" | \"intl\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"storageKey\" | \"disableLanguageSwitcher\" | \"isInvalid\" | \"autoSubmit\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\"> & ReactIntl.InjectedIntlProps>; }" ], "initialIsOpen": false }, @@ -480,7 +508,7 @@ "description": [], "source": { "path": "src/plugins/data/public/ui/search_bar/search_bar.tsx", - "lineNumber": 80 + "lineNumber": 84 }, "signature": [ "SearchBarOwnProps & SearchBarInjectedDeps" @@ -521,4 +549,4 @@ "misc": [], "objects": [] } -} \ No newline at end of file +} diff --git a/api_docs/navigation.json b/api_docs/navigation.json index 3417c074288a4..fce341c24ac79 100644 --- a/api_docs/navigation.json +++ b/api_docs/navigation.json @@ -482,7 +482,7 @@ }, "signature": [ "SearchBarOwnProps", - " & { appName: string; useDefaultBehaviors?: boolean | undefined; savedQueryId?: string | undefined; onSavedQueryIdChange?: ((savedQueryId?: string | undefined) => void) | undefined; } & Pick & { config?: TopNavMenuData[] | undefined; badges?: (({ iconType?: string | React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined; iconSide?: \"left\" | \"right\" | undefined; color?: string | undefined; isDisabled?: boolean | undefined; closeButtonProps?: Partial<", + " & { appName: string; useDefaultBehaviors?: boolean | undefined; savedQueryId?: string | undefined; onSavedQueryIdChange?: ((savedQueryId?: string | undefined) => void) | undefined; } & Pick & { config?: TopNavMenuData[] | undefined; badges?: (({ iconType?: string | React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined; iconSide?: \"left\" | \"right\" | undefined; color?: string | undefined; isDisabled?: boolean | undefined; closeButtonProps?: Partial<", "EuiIconProps", "> | undefined; } & ", "CommonProps", diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc index 7d48c593f9e18..17903408dff0e 100644 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ b/docs/canvas/canvas-expression-lifecycle.asciidoc @@ -177,8 +177,8 @@ Since all of the sub-expressions are now resolved into actual values, the < | Error | Buffer | { + custom: | Error | Buffer | Stream | { message: string | Error; attributes?: Record | undefined; - } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md deleted file mode 100644 index 67f2cf0cdcc7c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) - -## LegacyServiceSetupDeps.core property - -Signature: - -```typescript -core: LegacyCoreSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md deleted file mode 100644 index a5c1d59be06d3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) - -## LegacyServiceSetupDeps interface - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface LegacyServiceSetupDeps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup | | -| [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown> | | -| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | UiPlugins | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md deleted file mode 100644 index 032762904640b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) - -## LegacyServiceSetupDeps.plugins property - -Signature: - -```typescript -plugins: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md deleted file mode 100644 index d19a7dfcbfcfa..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) - -## LegacyServiceSetupDeps.uiPlugins property - -Signature: - -```typescript -uiPlugins: UiPlugins; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md deleted file mode 100644 index 17369e00a7068..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) > [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) - -## LegacyServiceStartDeps.core property - -Signature: - -```typescript -core: LegacyCoreStart; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md deleted file mode 100644 index d6f6b38b79f84..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) - -## LegacyServiceStartDeps interface - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface LegacyServiceStartDeps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) | LegacyCoreStart | | -| [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) | Record<string, unknown> | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md deleted file mode 100644 index 4634bf21fb42c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) > [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) - -## LegacyServiceStartDeps.plugins property - -Signature: - -```typescript -plugins: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index faac8108de825..3bbdf8c703ab1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -110,8 +110,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | -| [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | | -| [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | | | [LoggerContextConfigInput](./kibana-plugin-core-server.loggercontextconfiginput.md) | | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md new file mode 100644 index 0000000000000..a5694ea2d1af9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) + +## AutoRefreshDoneFn type + +Signature: + +```typescript +export declare type AutoRefreshDoneFn = () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md index 4d75dda61d5c9..521ceeb1e37f2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md @@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES | HISTOGRAM | "histogram" | | | IP | "ip" | | | IP\_RANGE | "ip_range" | | +| MISSING | "missing" | | | MURMUR3 | "murmur3" | | | NESTED | "nested" | | | NUMBER | "number" | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index d2e7ef9db05e8..4429f45f55645 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -47,6 +47,7 @@ | [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | | +| [waitUntilNextSessionCompletes$(sessionService, { waitForIdle })](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. | ## Interfaces @@ -92,6 +93,7 @@ | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | | [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | +| [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | ## Variables @@ -141,6 +143,7 @@ | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. | | [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* | +| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | | | [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md new file mode 100644 index 0000000000000..a221c3fe8ce61 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [autoSubmit](./kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md) + +## QueryStringInputProps.autoSubmit property + +Signature: + +```typescript +autoSubmit?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md index 38b8b0d2fa431..bd60f31bfe8f2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md @@ -14,6 +14,7 @@ export interface QueryStringInputProps | Property | Type | Description | | --- | --- | --- | +| [autoSubmit](./kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md) | boolean | | | [bubbleSubmitEvent](./kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md) | boolean | | | [className](./kibana-plugin-plugins-data-public.querystringinputprops.classname.md) | string | | | [dataTestSubj](./kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md) | string | | @@ -36,5 +37,6 @@ export interface QueryStringInputProps | [query](./kibana-plugin-plugins-data-public.querystringinputprops.query.md) | Query | | | [screenTitle](./kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md) | string | | | [size](./kibana-plugin-plugins-data-public.querystringinputprops.size.md) | SuggestionsListSize | | +| [storageKey](./kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md) | string | | | [submitOnBlur](./kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md new file mode 100644 index 0000000000000..dd77fe3ee8c32 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [storageKey](./kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md) + +## QueryStringInputProps.storageKey property + +Signature: + +```typescript +storageKey?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 193a2e5a24f3f..7c7f2a53aca92 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "isInvalid" | "storageKey" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "disableLanguageSwitcher" | "autoSubmit" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md new file mode 100644 index 0000000000000..a4b294fb1decd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [waitUntilNextSessionCompletes$](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) + +## waitUntilNextSessionCompletes$() function + +Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. + +Signature: + +```typescript +export declare function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| sessionService | ISessionService | [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| { waitForIdle } | WaitUntilNextSessionCompletesOptions | | + +Returns: + +`import("rxjs").Observable` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md new file mode 100644 index 0000000000000..d575722a22453 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) + +## WaitUntilNextSessionCompletesOptions interface + +Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) + +Signature: + +```typescript +export interface WaitUntilNextSessionCompletesOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) | number | For how long to wait between session state transitions before considering that session completed | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md new file mode 100644 index 0000000000000..60d3df7783852 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) > [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) + +## WaitUntilNextSessionCompletesOptions.waitForIdle property + +For how long to wait between session state transitions before considering that session completed + +Signature: + +```typescript +waitForIdle?: number; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md index be4c3705bd8de..40fa872ff0fc6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md @@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES | HISTOGRAM | "histogram" | | | IP | "ip" | | | IP\_RANGE | "ip_range" | | +| MISSING | "missing" | | | MURMUR3 | "murmur3" | | | NESTED | "nested" | | | NUMBER | "number" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 025cab9f48c1a..f4404521561d2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md index 5201444e69867..290dc10662569 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md @@ -9,9 +9,9 @@ Merges input$ and output$ streams and debounces emit till next macro-task. Could Signature: ```typescript -getUpdated$(): Readonly>; +getUpdated$(): Readonly>; ``` Returns: -`Readonly>` +`Readonly>` diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index 1bdc9b9dea859..5e6a60f019bea 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -12,7 +12,7 @@ When you've finished, you'll know how to: [float] === Required privileges When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. -For more information, refer to {ref}/security-privileges.html[Security privileges]. +Learn how to <>, or refer to {ref}/security-privileges.html[Security privileges] for more information. [float] [[set-up-on-cloud]] @@ -141,3 +141,5 @@ For more information, refer to <>. If you are you ready to add your own data, refer to <>. If you want to ingest your data, refer to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. + +If you want to secure access to your data, refer to our guide on <> diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index fb4250368086e..0218bac58815a 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,30 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +[discrete] +[[import-geospatial-privileges]] +=== Security privileges + +The {stack-security-features} provide roles and privileges that control which users can upload files. +You can manage your roles, privileges, and +spaces in **{stack-manage-app}** in {kib}. For more information, see +{ref}/security-privileges.html[Security privileges], +<>, and <>. + +To upload GeoJSON files in {kib} with *Maps*, you must have: + +* The `all` {kib} privilege for *Maps*. +* The `all` {kib} privilege for *Index Pattern Management*. +* The `create` and `create_index` index privileges for destination indices. +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. + +To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: + +* The `manage_pipeline` cluster privilege. +* The `read` {kib} privilege for *Machine Learning*. +* The `machine_learning_admin` or `machine_learning_user` role. + + [discrete] === Upload CSV with latitude and longitude columns diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index acb343191609d..d62e3c3eb88aa 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -52,7 +52,7 @@ for example, `logstash-*`. ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. -*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a date modifier: +*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a {kibana-ref}/logging-service.html#date-format[date modifier]: [source,yaml] ------------------- logging: @@ -87,7 +87,7 @@ See https://github.com/elastic/kibana/pull/87939 for more details. [float] ==== Logging destination is specified by the appender -*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using appenders. +*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using {kibana-ref}/logging-service.html#logging-appenders[appenders]. *Impact:* To restore the previous behavior and log records to *stdout*, in `kibana.yml` use an appender with `type: console`. [source,yaml] @@ -118,7 +118,7 @@ logging: [float] ==== Set log verbosity with root -*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required log level. +*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required {kibana-ref}/logging-service.html#log-level[log level]. *Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level`: [source,yaml] @@ -175,7 +175,7 @@ logging: ==== Configure log rotation with the rolling-file appender *Details:* Previously log rotation would be enabled when `logging.rotate.enabled` was true. -*Impact:* To restore the previous behavior, in `kibana.yml` use the `rolling-file` appender. +*Impact:* To restore the previous behavior, in `kibana.yml` use the {kibana-ref}/logging-service.html#rolling-file-appender[`rolling-file`] appender. [source,yaml] ------------------- diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index e4d2b53a2d8d6..5d0242ae31950 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -286,3 +286,9 @@ This content has moved. See {ref}/ingest.html[Ingest pipelines]. == Timelion This content has moved. refer to <>. + + +[role="exclude",id="space-rbac-tutorial"] +== Tutorial: Use role-based access control to customize Kibana spaces + +This content has moved. refer to <>. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 3645499d5f9ff..20bbbcf874c05 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -59,6 +59,12 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.actions.proxyUrl` {ess-icon} | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. +| `xpack.actions.proxyBypassHosts` {ess-icon} + | Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. + +| `xpack.actions.proxyOnlyHosts` {ess-icon} + | Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. + | `xpack.actions.proxyHeaders` {ess-icon} | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. @@ -71,6 +77,13 @@ a|`xpack.actions.` + As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates. +| `xpack.actions.maxResponseContentLength` {ess-icon} + | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB). + +| `xpack.actions.responseTimeout` {ess-icon} + | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as [ms|s|m|h|d|w|M|Y], for example, '20m', '24h', '7d', '1w'. Defaults to 60s. + + |=== [float] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 73b268e1e48b3..90e813afad6f4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -281,7 +281,7 @@ To reload the logging settings, send a SIGHUP signal to {kib}. |=== |[[logging-root]] `logging.root:` -| The `root` logger has a dedicated configuration node since this context name is special and is pre-configured for logging by default. +| The {kibana-ref}/logging-service.html#logging-service[`root` logger] has a dedicated configuration node since this context name is special and is pre-configured for logging by default. // TODO: add link to the advanced logging documentation. |[[logging-root-appenders]] `logging.root.appenders:` @@ -303,7 +303,7 @@ To reload the logging settings, send a SIGHUP signal to {kib}. | Specific appender format to apply for a particular logger context. | `logging.appenders:` -| Define how and where log messages are displayed (eg. *stdout* or console) and stored (eg. file on the disk). +| {kibana-ref}/logging-service.html#logging-appenders[Appenders] define how and where log messages are displayed (eg. *stdout* or console) and stored (eg. file on the disk). // TODO: add link to the advanced logging documentation. | `logging.appenders.console:` @@ -429,6 +429,15 @@ to display map tiles in tilemap visualizations. By default, override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` +| `migrations.batchSize:` + | Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If the migration fails due to a `circuit_breaking_exception`, set a smaller `batchSize` value. *Default: `1000`* + +| `migrations.enableV2:` + | experimental[]. Enables the new Saved Objects migration algorithm. For information about the migration algorithm, refer to <>. When `migrations v2` is stable, the setting will be removed in an upcoming release without any further notice. Setting the value to `false` causes {kib} to use the legacy migration algorithm, which shipped in 7.11 and earlier versions. *Default: `true`* + +| `migrations.retryAttempts:` + | The number of times migrations retry temporary failures, such as a network timeout, 503 status code, or `snapshot_in_progress_exception`. When upgrade migrations frequently fail after exhausting all retry attempts with a message such as `Unable to complete the [...] step after 15 attempts, terminating.`, increase the setting value. *Default: `15`* + | `newsfeed.enabled:` | Controls whether to enable the newsfeed system for the {kib} UI notification center. Set to `false` to disable the diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 8603ca9935cac..fdcd71791ad3a 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -50,6 +50,16 @@ For large deployments with more than 10 {kib} instances and more than 10 000 sav ==== Preventing migration failures This section highlights common causes of {kib} upgrade failures and how to prevent them. +[float] +===== timeout_exception or receive_timeout_transport_exception +There is a known issue in v7.12.0 for users who tried the fleet beta. Upgrade migrations fail because of a large number of documents in the `.kibana` index. + +This can cause Kibana to log errors like: +> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms] +> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] + +See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. + [float] ===== Corrupt saved objects We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 80ce77f30c75e..ff71cd7b383bd 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -33,6 +33,40 @@ If the value of your parameter contains spaces or commas you have to put the val .es(q='some query', index=logstash-*) +[float] +[[customize-data-series-y-axis]] +===== .yaxis() function + +{kib} supports many y-axis scales and ranges for your data series. + +The `.yaxis()` function supports the following parameters: + +* *yaxis* — The numbered y-axis to plot the series on. For example, use `.yaxis(2)` to display a second y-axis. +* *min* — The minimum value for the y-axis range. +* *max* — The maximum value for the y-axis range. +* *position* — The location of the units. Values include `left` or `right`. +* *label* — The label for the axis. +* *color* — The color of the axis label. +* *units* — The function to use for formatting the y-axis labels. Values include `bits`, `bits/s`, `bytes`, `bytes/s`, `currency(:ISO 4217 currency code)`, `percent`, and `custom(:prefix:suffix)`. +* *tickDecimals* — The tick decimal precision. + +Example: + +[source,text] +---------------------------------- +.es(index= kibana_sample_data_logs, + timefield='@timestamp', + metric='avg:bytes') + .label('Average Bytes for request') + .title('Memory consumption over time in bytes').yaxis(1,units=bytes,position=left), <1> +.es(index= kibana_sample_data_logs, + timefield='@timestamp', + metric=avg:machine.ram) + .label('Average Machine RAM amount').yaxis(2,units=bytes,position=right) <2> +---------------------------------- + +<1> `.yaxis(1,units=bytes,position=left)` — Specifies the first y-axis for the first data series, and changes the units on the left. +<2> `.yaxis(2,units=bytes,position=left)` — Specifies the second y-axis for the second data series, and changes the units on the right. [float] ==== Tutorial: Create visualizations with Timelion diff --git a/docs/user/security/images/role-index-privilege.png b/docs/user/security/images/role-index-privilege.png deleted file mode 100644 index 1dc1ae640e3ba..0000000000000 Binary files a/docs/user/security/images/role-index-privilege.png and /dev/null differ diff --git a/docs/user/security/images/role-management.png b/docs/user/security/images/role-management.png deleted file mode 100644 index 29efdd85c4df3..0000000000000 Binary files a/docs/user/security/images/role-management.png and /dev/null differ diff --git a/docs/user/security/images/role-new-user.png b/docs/user/security/images/role-new-user.png deleted file mode 100644 index c882eeea42d60..0000000000000 Binary files a/docs/user/security/images/role-new-user.png and /dev/null differ diff --git a/docs/user/security/images/role-space-visualization.png b/docs/user/security/images/role-space-visualization.png deleted file mode 100644 index 36f83f09f064b..0000000000000 Binary files a/docs/user/security/images/role-space-visualization.png and /dev/null differ diff --git a/docs/user/security/images/tutorial-secure-access-example-1-role.png b/docs/user/security/images/tutorial-secure-access-example-1-role.png new file mode 100644 index 0000000000000..53540da7170ea Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-role.png differ diff --git a/docs/user/security/images/tutorial-secure-access-example-1-space.png b/docs/user/security/images/tutorial-secure-access-example-1-space.png new file mode 100644 index 0000000000000..a48fdeaa6efa1 Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-space.png differ diff --git a/docs/user/security/images/tutorial-secure-access-example-1-test.png b/docs/user/security/images/tutorial-secure-access-example-1-test.png new file mode 100644 index 0000000000000..305b97017a9d8 Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-test.png differ diff --git a/docs/user/security/images/tutorial-secure-access-example-1-user.png b/docs/user/security/images/tutorial-secure-access-example-1-user.png new file mode 100644 index 0000000000000..8df26cf28ef16 Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-user.png differ diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index 6a5c4a83aa3ad..71c5bd268a67d 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -47,4 +47,3 @@ include::authorization/kibana-privileges.asciidoc[] include::api-keys/index.asciidoc[] include::encryption-keys/index.asciidoc[] include::role-mappings/index.asciidoc[] -include::rbac_tutorial.asciidoc[] diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc deleted file mode 100644 index 6324539c3c10a..0000000000000 --- a/docs/user/security/rbac_tutorial.asciidoc +++ /dev/null @@ -1,105 +0,0 @@ -[[space-rbac-tutorial]] -=== Tutorial: Use role-based access control to customize Kibana spaces - -With role-based access control (RBAC), you can provide users access to data, tools, -and Kibana spaces. In this tutorial, you will learn how to configure roles -that provide the right users with the right access to the data, tools, and -Kibana spaces. - -[float] -==== Scenario - -Our user is a web developer working on a bank's -online mortgage service. The web developer has these -three requirements: - -* Have access to the data for that service -* Build visualizations and dashboards -* Monitor the performance of the system - -You'll provide the web developer with the access and privileges to get the job done. - -[float] -==== Prerequisites - -To complete this tutorial, you'll need the following: - -* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. -* **A space**: In this tutorial, use `Dev Mortgage` as the space -name. See <> for -details on creating a space. -* **Data**: You can use <> or -live data. In the following steps, Filebeat and Metricbeat data are used. - -[float] -==== Steps - -With the requirements in mind, here are the steps that you will work -through in this tutorial: - -* Create a role named `mortgage-developer` -* Give the role permission to access the data in the relevant indices -* Give the role permission to create visualizations and dashboards -* Create the web developer's user account with the proper roles - -[float] -==== Create a role - -Open the main menu, then click *Stack Management > Roles* -for an overview of your roles. This view provides actions -for you to create, edit, and delete roles. - -[role="screenshot"] -image::security/images/role-management.png["Role management"] - - -You can create as many roles as you like. Click *Create role* and -provide a name. Use `dev-mortgage` because this role is for a developer -working on the bank's mortgage application. - - -[float] -==== Give the role permission to access the data - -Access to data in indices is an index-level privilege, so in -*Index privileges*, add lines for the indices that contain the -data for this role. Two privileges are required: `read` and -`view_index_metadata`. All privileges are detailed in the -https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation. - -In the screenshots, Filebeat and Metricbeat data is used, but you -should use the index patterns for your indices. - -[role="screenshot"] -image::security/images/role-index-privilege.png["Index privilege"] - -[float] -==== Give the role permissions to {kib} apps - -To enable users to create dashboards, visualizations, and saved searches, add {kib} privileges to the `dev-mortgage` role. - -. On the *{kib} privileges* window, select *Dev Mortgage* from the *Space* dropdown. - -. Click **Add space privilege**. - -. For *Dashboard*, *Visualize Library*, and *Discover*, click *All*. -+ -It is common to create saved searches in *Discover* while creating visualizations. -+ -[role="screenshot"] -image::security/images/role-space-visualization.png["Associate space"] - -[float] -==== Create the developer user account with the proper roles - -. Open the main menu, then click *Stack Management > Users*. -. Click **Create user**, then give the user the `dev-mortgage` -and `monitoring-user` roles, which are required for *Stack Monitoring* users. - -[role="screenshot"] -image::security/images/role-new-user.png["Developer user"] - -Finally, have the developer log in and access the Dev Mortgage space -and create a new visualization. - -NOTE: If the user is assigned to only one space, they will automatically enter that space on login. diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc new file mode 100644 index 0000000000000..63b83712e3e6e --- /dev/null +++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc @@ -0,0 +1,136 @@ +[[tutorial-secure-access-to-kibana]] +== Securing access to {kib} + + +{kib} is home to an ever-growing suite of powerful features, which help you get the most out of your data. Your data is important, and should be protected. {kib} allows you to secure access to your data and control how users are able to interact with your data. + +For example, some users might only need to view your stunning dashboards, while others might need to manage your fleet of Elastic agents and run machine learning jobs to detect anomalous behavior in your network. + +This guide introduces you to three of {kib}'s security features: spaces, roles, and users. By the end of this tutorial, you will learn how to manage these entities, and how you can leverage them to secure access to both {kib} and your data. + +[float] +=== Spaces + +Do you have multiple teams using {kib}? Do you want a “playground” to experiment with new visualizations or alerts? If so, then <> can help. + +Think of a space as another instance of {kib}. A space allows you to organize your <>, <>, <>, and much more into their own categories. For example, you might have a Marketing space for your marketeers to track the results of their campaigns, and an Engineering space for your developers to {apm-get-started-ref}/overview.html[monitor application performance]. + +The assets you create in one space are isolated from other spaces, so when you enter a space, you only see the assets that belong to that space. + +Refer to the <> for more information. + +[float] +=== Roles + +Once your spaces are setup, the next step to securing access is to provision your roles. Roles are a collection of privileges that allow you to perform actions in {kib} and Elasticsearch. Roles are assigned to users, and to {ref}/built-in-users.html[system accounts] that power the Elastic Stack. + +You can create your own roles, or use any of the {ref}/built-in-roles.html[built-in roles]. Some built-in roles are intended for Elastic Stack components and should not be assigned to end users directly. + +One of the more useful built-in roles is `kibana_admin`. Assigning this role to your users will grant access to all of {kib}'s features. This includes the ability to manage Spaces. + +The built-in roles are great for getting started with the Elastic Stack, and for system administrators who do not need more restrictive access. With so many features, it’s not possible to ship more granular roles to accommodate everyone’s needs. This is where custom roles come in. + +As an administrator, you have the ability to create your own roles to describe exactly the kind of access your users should have. For example, you might create a `marketing_user` role, which you then assign to all users in your marketing department. This role would grant access to all of the necessary data and features for this team to be successful, without granting them access they don’t require. + + +[float] +=== Users + +Once your roles are setup, the next step to securing access is to create your users, and assign them one or more roles. {kib}'s user management allows you to provision accounts for each of your users. + +TIP: Want Single Sign-on? {kib} supports a wide range of SSO implementations, including SAML, OIDC, LDAP/AD, and Kerberos. <>. + + +[float] +[[tutorial-secure-kibana-dashboards-only]] +=== Example: Create a user with access only to dashboards + +Let’s work through an example together. Consider a marketing analyst who wants to monitor the effectiveness of their campaigns. They should be able to see their team’s dashboards, but not be allowed to view or manage anything else in {kib}. All of the team’s dashboards are located in the Marketing space. + +[float] +==== Create a space + +Create a Marketing space for your marketing analysts to use. + +. Open the main menu, and select **Stack Management**. +. Under **{kib}**, select **Spaces**. +. Click **Create a space**. +. Give this space a unique name. For example: `Marketing`. +. Click **Create space**. ++ +If you’ve followed the example above, you should end up with a space that looks like this: ++ +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-space.png[Create space UI] + + +[float] +==== Create a role + +To effectively use dashboards, create a role that describes the privileges you want to grant. +In this example, a marketing analyst will need: + +* Access to **read** the data that powers the dashboards +* Access to **read** the dashboards within the `Marketing` space + +To create the role: + +. Open the main menu, and select **Stack Management**. +. Under **Security**, select **Roles**. +. Click **Create role**. +. Give this role a unique name. For example: `marketing_dashboards_role`. +. For this example, you want to store all marketing data in the `acme-marketing-*` set of indices. To grant this access, locate the **Index privileges** section and enter: +.. `acme-marketing-*` in the **Indices** field. +.. `read` and `view_index_metadata` in the **Privileges** field. ++ +TIP: You can add multiple patterns of indices, and grant different access levels to each. Click **Add index privilege** to grant additional access. +. To grant access to dashboards in the `Marketing` space, locate the {kib} section, and click **Add {kib} privilege**: +.. From the **Spaces** dropdown, select the `Marketing` space. +.. Expand the **Analytics** section, and select the **Read** privilege for **Dashboard**. +.. Click **Add Kibana privilege**. +. Click **Create role**. ++ +If you’ve followed the example above, you should end up with a role that looks like this: ++ +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-role.png[Create role UI] + + +[float] +==== Create a user + +Now that you created a role, create a user account. + +. Navigate to *Stack Management*, and under *Security*, select *Users*. +. Click *Create user*. +. Give this user a descriptive username, and choose a secure password. +. Assign the *marketing_dashboards_role* that you previously created to this new user. +. Click *Create user*. + +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-user.png[Create user UI] + +[float] +==== Verify + +Verify that the user and role are working correctly. + +. Logout of {kib} if you are already logged in. +. In the login screen, enter the username and password for the account you created. ++ +You’re taken into the `Marketing` space, and the main navigation shows only the *Dashboard* application. ++ +[role="screenshot"] +image::user/security/images/tutorial-secure-access-example-1-test.png[Verifying access to dashboards] + + +[float] +=== What's next? + +This guide is an introduction to {kib}'s security features. Check out these additional resources to learn more about authenticating and authorizing your users. + +* View the <> to learn more about single-sign on and other login features. + +* View the <> to learn more about authorizing access to {kib}'s features. + +Still have questions? Ask on our https://discuss.elastic.co/c/kibana[Kibana discuss forum] and a fellow community member or Elastic engineer will help out. diff --git a/docs/user/setup.asciidoc b/docs/user/setup.asciidoc index a38bf699c1db8..bea13c1ef49b2 100644 --- a/docs/user/setup.asciidoc +++ b/docs/user/setup.asciidoc @@ -54,6 +54,8 @@ include::{kib-repo-dir}/setup/start-stop.asciidoc[] include::{kib-repo-dir}/setup/access.asciidoc[] +include::security/tutorials/how-to-secure-access-to-kibana.asciidoc[] + include::{kib-repo-dir}/setup/connect-to-elasticsearch.asciidoc[] include::{kib-repo-dir}/setup/upgrade.asciidoc[] diff --git a/jest.config.js b/jest.config.js index 03dc832ba170c..bd1e865a7e64a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,6 @@ module.exports = { projects: [ '/packages/*/jest.config.js', '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', '/src/plugins/*/jest.config.js', '/test/*/jest.config.js', '/x-pack/plugins/*/jest.config.js', diff --git a/kibana.d.ts b/kibana.d.ts index a2c670c96a699..8a7a531890057 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -13,18 +13,3 @@ import * as Public from 'src/core/public'; import * as Server from 'src/core/server'; export { Public, Server }; - -/** - * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). - */ -import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; - -/** - * Re-export legacy types under a namespace. - */ -export namespace Legacy { - export type KibanaConfig = LegacyKibanaServer.KibanaConfig; - export type Request = LegacyKibanaServer.Request; - export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; - export type Server = LegacyKibanaServer.Server; -} diff --git a/package.json b/package.json index 34e044140d297..9bddca4665467 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", + "globby/fast-glob": "3.2.5", "**/graphql-toolkit/lodash": "^4.17.21", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-fetch/node-fetch": "^2.6.1", @@ -91,13 +92,13 @@ "**/typescript": "4.1.3" }, "engines": { - "node": "14.16.0", + "node": "14.16.1", "yarn": "^1.21.1" }, "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "27.0.0", + "@elastic/charts": "28.0.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", @@ -130,10 +131,12 @@ "@kbn/crypto": "link:packages/kbn-crypto", "@kbn/i18n": "link:packages/kbn-i18n", "@kbn/interpreter": "link:packages/kbn-interpreter", + "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:packages/kbn-legacy-logging", "@kbn/logging": "link:packages/kbn-logging", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", + "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:packages/kbn-std", "@kbn/tinymath": "link:packages/kbn-tinymath", "@kbn/ui-framework": "link:packages/kbn-ui-framework", @@ -205,8 +208,6 @@ "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", "core-js": "^3.6.5", - "css-minimizer-webpack-plugin": "^1.3.0", - "custom-event-polyfill": "^0.3.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", @@ -240,7 +241,7 @@ "github-markdown-css": "^2.10.0", "glob": "^7.1.2", "glob-all": "^3.2.1", - "globby": "^8.0.1", + "globby": "^11.0.3", "graphql": "^0.13.2", "graphql-fields": "^1.0.2", "graphql-tag": "^2.10.3", @@ -443,7 +444,7 @@ "@bazel/ibazel": "^0.14.0", "@bazel/typescript": "^3.2.3", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.5.0", + "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", @@ -534,7 +535,6 @@ "@types/getos": "^3.0.0", "@types/git-url-parse": "^9.0.0", "@types/glob": "^7.1.2", - "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", @@ -682,7 +682,8 @@ "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", "css-loader": "^3.4.2", - "cypress": "^6.2.1", + "css-minimizer-webpack-plugin": "^1.3.0", + "cypress": "^6.8.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "cypress-pipe": "^2.0.0", diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index c2e579e7fdbea..80a2255d71805 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -7,6 +7,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, + "isolatedModules": true, "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts index ab113b96a5f03..ff25f2a7bf55e 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts @@ -27,8 +27,6 @@ it('produces the right watch and ignore list', () => { expect(watchPaths).toMatchInlineSnapshot(` Array [ /src/core, - /src/legacy/server, - /src/legacy/utils, /config, /x-pack/test/plugin_functional/plugins/resolver_test, /src/plugins, diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 2801e0a0688cc..17ac75e9f3d9e 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -1,69 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get correctly handles server config.: default 1`] = ` -Object { - "autoListen": true, - "basePath": "/abc", - "compression": Object { - "enabled": true, - }, - "cors": false, - "customResponseHeaders": Object { - "custom-header": "custom-value", - }, - "host": "host", - "keepaliveTimeout": 5000, - "maxPayload": 1000, - "name": "kibana-hostname", - "port": 1234, - "publicBaseUrl": "https://myhost.com/abc", - "rewriteBasePath": false, - "socketTimeout": 2000, - "ssl": Object { - "enabled": true, - "keyPassphrase": "some-phrase", - "someNewValue": "new", - }, - "uuid": undefined, - "xsrf": Object { - "allowlist": Array [], - "disableProtection": false, - }, -} -`; - -exports[`#get correctly handles server config.: disabled ssl 1`] = ` -Object { - "autoListen": true, - "basePath": "/abc", - "compression": Object { - "enabled": true, - }, - "cors": false, - "customResponseHeaders": Object { - "custom-header": "custom-value", - }, - "host": "host", - "keepaliveTimeout": 5000, - "maxPayload": 1000, - "name": "kibana-hostname", - "port": 1234, - "publicBaseUrl": "http://myhost.com/abc", - "rewriteBasePath": false, - "socketTimeout": 2000, - "ssl": Object { - "certificate": "cert", - "enabled": false, - "key": "key", - }, - "uuid": undefined, - "xsrf": Object { - "allowlist": Array [], - "disableProtection": false, - }, -} -`; - exports[`#get correctly handles silent logging config. 1`] = ` Object { "appenders": Object { @@ -78,6 +14,7 @@ Object { "root": Object { "level": "off", }, + "silent": true, } `; @@ -93,10 +30,13 @@ Object { "type": "legacy-appender", }, }, + "dest": "/some/path.log", + "json": true, "loggers": undefined, "root": Object { "level": "all", }, + "verbose": true, } `; diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 5dd1941545708..47151503e1634 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -65,59 +65,6 @@ describe('#get', () => { expect(configAdapter.get('logging')).toMatchSnapshot(); }); - - test('correctly handles server config.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - server: { - name: 'kibana-hostname', - autoListen: true, - basePath: '/abc', - cors: false, - customResponseHeaders: { 'custom-header': 'custom-value' }, - host: 'host', - maxPayloadBytes: 1000, - keepaliveTimeout: 5000, - socketTimeout: 2000, - port: 1234, - publicBaseUrl: 'https://myhost.com/abc', - rewriteBasePath: false, - ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' }, - compression: { enabled: true }, - someNotSupportedValue: 'val', - xsrf: { - disableProtection: false, - allowlist: [], - }, - }, - }); - - const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({ - server: { - name: 'kibana-hostname', - autoListen: true, - basePath: '/abc', - cors: false, - customResponseHeaders: { 'custom-header': 'custom-value' }, - host: 'host', - maxPayloadBytes: 1000, - keepaliveTimeout: 5000, - socketTimeout: 2000, - port: 1234, - publicBaseUrl: 'http://myhost.com/abc', - rewriteBasePath: false, - ssl: { enabled: false, certificate: 'cert', key: 'key' }, - compression: { enabled: true }, - someNotSupportedValue: 'val', - xsrf: { - disableProtection: false, - allowlist: [], - }, - }, - }); - - expect(configAdapter.get('server')).toMatchSnapshot('default'); - expect(configAdapterWithDisabledSSL.get('server')).toMatchSnapshot('disabled ssl'); - }); }); describe('#set', () => { diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index 8ec26ff1f8e71..bc6fd49e2498a 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -9,15 +9,6 @@ import { ConfigPath } from '../config'; import { ObjectToConfigAdapter } from '../object_to_config_adapter'; -// TODO: fix once core schemas are moved to this package -type LoggingConfigType = any; - -/** - * @internal - * @deprecated - */ -export type LegacyVars = Record; - /** * Represents logging config supported by the legacy platform. */ @@ -30,7 +21,7 @@ export interface LegacyLoggingConfig { events?: Record; } -type MixedLoggingConfig = LegacyLoggingConfig & Partial; +type MixedLoggingConfig = LegacyLoggingConfig & Record; /** * Represents adapter between config provided by legacy platform and `Config` @@ -48,6 +39,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { }, root: { level: 'info', ...root }, loggers, + ...legacyLoggingConfig, }; if (configValue.silent) { @@ -61,47 +53,11 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { return loggingConfig; } - private static transformServer(configValue: any = {}) { - // TODO: New platform uses just a subset of `server` config from the legacy platform, - // new values will be exposed once we need them - return { - autoListen: configValue.autoListen, - basePath: configValue.basePath, - cors: configValue.cors, - customResponseHeaders: configValue.customResponseHeaders, - host: configValue.host, - maxPayload: configValue.maxPayloadBytes, - name: configValue.name, - port: configValue.port, - publicBaseUrl: configValue.publicBaseUrl, - rewriteBasePath: configValue.rewriteBasePath, - ssl: configValue.ssl, - keepaliveTimeout: configValue.keepaliveTimeout, - socketTimeout: configValue.socketTimeout, - compression: configValue.compression, - uuid: configValue.uuid, - xsrf: configValue.xsrf, - }; - } - - private static transformPlugins(configValue: LegacyVars = {}) { - // These properties are the only ones we use from the existing `plugins` config node - // since `scanDirs` isn't respected by new platform plugin discovery. - return { - initialize: configValue.initialize, - paths: configValue.paths, - }; - } - public get(configPath: ConfigPath) { const configValue = super.get(configPath); switch (configPath) { case 'logging': return LegacyObjectToConfigAdapter.transformLogging(configValue as LegacyLoggingConfig); - case 'server': - return LegacyObjectToConfigAdapter.transformServer(configValue); - case 'plugins': - return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars); default: return configValue; } diff --git a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts index 26b1a6fa2e804..2381faefbff29 100644 --- a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts @@ -9,6 +9,7 @@ import Path from 'path'; import globby from 'globby'; +import normalize from 'normalize-path'; import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; @@ -32,7 +33,7 @@ export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPa ), ...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)), ]) - ); + ).map((path) => normalize(path)); const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => // absolute paths returned from globby are using normalize or diff --git a/src/legacy/utils/jest.config.js b/packages/kbn-io-ts-utils/jest.config.js similarity index 85% rename from src/legacy/utils/jest.config.js rename to packages/kbn-io-ts-utils/jest.config.js index 593c3aec9d0b0..1a71166fae843 100644 --- a/src/legacy/utils/jest.config.js +++ b/packages/kbn-io-ts-utils/jest.config.js @@ -8,6 +8,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/utils'], + rootDir: '../..', + roots: ['/packages/kbn-io-ts-utils'], }; diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json new file mode 100644 index 0000000000000..4d6f02d3f85a6 --- /dev/null +++ b/packages/kbn-io-ts-utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/io-ts-utils", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + } +} diff --git a/src/core/server/utils/index.ts b/packages/kbn-io-ts-utils/src/index.ts similarity index 74% rename from src/core/server/utils/index.ts rename to packages/kbn-io-ts-utils/src/index.ts index b0776c48f3bed..2032127b1eb91 100644 --- a/src/core/server/utils/index.ts +++ b/packages/kbn-io-ts-utils/src/index.ts @@ -6,5 +6,6 @@ * Side Public License, v 1. */ -export * from './from_root'; -export * from './package_json'; +export { jsonRt } from './json_rt'; +export { mergeRt } from './merge_rt'; +export { strictKeysRt } from './strict_keys_rt'; diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts similarity index 85% rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts rename to packages/kbn-io-ts-utils/src/json_rt/index.test.ts index d6c286c672d90..1220639fc7bef 100644 --- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts +++ b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; @@ -12,9 +13,7 @@ import { Right } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; -function getValueOrThrow>( - either: TEither -): Right { +function getValueOrThrow>(either: TEither): Right { const value = pipe( either, fold(() => { diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts b/packages/kbn-io-ts-utils/src/json_rt/index.ts similarity index 74% rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.ts rename to packages/kbn-io-ts-utils/src/json_rt/index.ts index 0207145a17be7..bc596d53db54c 100644 --- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/json_rt/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts similarity index 66% rename from x-pack/plugins/apm/common/runtime_types/merge/index.test.ts rename to packages/kbn-io-ts-utils/src/merge_rt/index.test.ts index af5a0221662d5..b25d4451895f2 100644 --- a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts +++ b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts @@ -1,18 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; import { isLeft } from 'fp-ts/lib/Either'; -import { merge } from './'; +import { mergeRt } from '.'; import { jsonRt } from '../json_rt'; describe('merge', () => { it('fails on one or more errors', () => { - const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]); + const type = mergeRt(t.type({ foo: t.string }), t.type({ bar: t.number })); const result = type.decode({ foo: '' }); @@ -20,10 +21,7 @@ describe('merge', () => { }); it('merges left to right', () => { - const typeBoolean = merge([ - t.type({ foo: t.string }), - t.type({ foo: jsonRt.pipe(t.boolean) }), - ]); + const typeBoolean = mergeRt(t.type({ foo: t.string }), t.type({ foo: jsonRt.pipe(t.boolean) })); const resultBoolean = typeBoolean.decode({ foo: 'true', @@ -34,10 +32,7 @@ describe('merge', () => { foo: true, }); - const typeString = merge([ - t.type({ foo: jsonRt.pipe(t.boolean) }), - t.type({ foo: t.string }), - ]); + const typeString = mergeRt(t.type({ foo: jsonRt.pipe(t.boolean) }), t.type({ foo: t.string })); const resultString = typeString.decode({ foo: 'true', @@ -50,10 +45,10 @@ describe('merge', () => { }); it('deeply merges values', () => { - const type = merge([ + const type = mergeRt( t.type({ foo: t.type({ baz: t.string }) }), - t.type({ foo: t.type({ bar: t.string }) }), - ]); + t.type({ foo: t.type({ bar: t.string }) }) + ); const result = type.decode({ foo: { diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.ts similarity index 62% rename from x-pack/plugins/apm/common/runtime_types/merge/index.ts rename to packages/kbn-io-ts-utils/src/merge_rt/index.ts index 451edf678aabe..c582767fb5101 100644 --- a/x-pack/plugins/apm/common/runtime_types/merge/index.ts +++ b/packages/kbn-io-ts-utils/src/merge_rt/index.ts @@ -1,31 +1,40 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; import { merge as lodashMerge } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; -import { ValuesType } from 'utility-types'; -export type MergeType< - T extends t.Any[], - U extends ValuesType = ValuesType -> = t.Type & { - _tag: 'MergeType'; - types: T; -}; +type PlainObject = Record; + +type DeepMerge = U extends PlainObject + ? T extends PlainObject + ? Omit & + { + [key in keyof U]: T extends { [k in key]: any } ? DeepMerge : U[key]; + } + : U + : U; // this is similar to t.intersection, but does a deep merge // instead of a shallow merge -export function merge( - types: [A, B] -): MergeType<[A, B]>; +export type MergeType = t.Type< + DeepMerge, t.TypeOf>, + DeepMerge, t.OutputOf> +> & { + _tag: 'MergeType'; + types: [T1, T2]; +}; + +export function mergeRt(a: T1, b: T2): MergeType; -export function merge(types: t.Any[]) { +export function mergeRt(...types: t.Any[]) { const mergeType = new t.Type( 'merge', (u): u is unknown => { diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts similarity index 77% rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts index 4212e0430ff5f..ab20ca42a283e 100644 --- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; @@ -14,10 +15,7 @@ describe('strictKeysRt', () => { it('correctly and deeply validates object keys', () => { const checks: Array<{ type: t.Type; passes: any[]; fails: any[] }> = [ { - type: t.intersection([ - t.type({ foo: t.string }), - t.partial({ bar: t.string }), - ]), + type: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.string })]), passes: [{ foo: '' }, { foo: '', bar: '' }], fails: [ { foo: '', unknownKey: '' }, @@ -26,15 +24,9 @@ describe('strictKeysRt', () => { }, { type: t.type({ - path: t.union([ - t.type({ serviceName: t.string }), - t.type({ transactionType: t.string }), - ]), + path: t.union([t.type({ serviceName: t.string }), t.type({ transactionType: t.string })]), }), - passes: [ - { path: { serviceName: '' } }, - { path: { transactionType: '' } }, - ], + passes: [{ path: { serviceName: '' } }, { path: { transactionType: '' } }], fails: [ { path: { serviceName: '', unknownKey: '' } }, { path: { transactionType: '', unknownKey: '' } }, @@ -62,9 +54,7 @@ describe('strictKeysRt', () => { if (!isRight(result)) { throw new Error( - `Expected ${JSON.stringify( - value - )} to be allowed, but validation failed with ${ + `Expected ${JSON.stringify(value)} to be allowed, but validation failed with ${ result.left[0].message }` ); @@ -76,9 +66,7 @@ describe('strictKeysRt', () => { if (!isLeft(result)) { throw new Error( - `Expected ${JSON.stringify( - value - )} to be disallowed, but validation succeeded` + `Expected ${JSON.stringify(value)} to be disallowed, but validation succeeded` ); } }); diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts similarity index 66% rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts index e90ccf7eb8d31..56afdf54463f7 100644 --- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts @@ -1,14 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import * as t from 'io-ts'; import { either, isRight } from 'fp-ts/lib/Either'; import { mapValues, difference, isPlainObject, forEach } from 'lodash'; -import { MergeType, merge } from '../merge'; +import { MergeType, mergeRt } from '../merge_rt'; /* Type that tracks validated keys, and fails when the input value @@ -21,7 +22,7 @@ type ParsableType = | t.PartialType | t.ExactType | t.InterfaceType - | MergeType; + | MergeType; function getKeysInObject>( object: T, @@ -32,17 +33,16 @@ function getKeysInObject>( const ownPrefix = prefix ? `${prefix}.${key}` : key; keys.push(ownPrefix); if (isPlainObject(object[key])) { - keys.push( - ...getKeysInObject(object[key] as Record, ownPrefix) - ); + keys.push(...getKeysInObject(object[key] as Record, ownPrefix)); } }); return keys; } -function addToContextWhenValidated< - T extends t.InterfaceType | t.PartialType ->(type: T, prefix: string): T { +function addToContextWhenValidated | t.PartialType>( + type: T, + prefix: string +): T { const validate = (input: unknown, context: t.Context) => { const result = type.validate(input, context); const keysType = context[0].type as StrictKeysType; @@ -50,36 +50,19 @@ function addToContextWhenValidated< throw new Error('Expected a top-level StrictKeysType'); } if (isRight(result)) { - keysType.trackedKeys.push( - ...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`) - ); + keysType.trackedKeys.push(...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`)); } return result; }; if (type._tag === 'InterfaceType') { - return new t.InterfaceType( - type.name, - type.is, - validate, - type.encode, - type.props - ) as T; + return new t.InterfaceType(type.name, type.is, validate, type.encode, type.props) as T; } - return new t.PartialType( - type.name, - type.is, - validate, - type.encode, - type.props - ) as T; + return new t.PartialType(type.name, type.is, validate, type.encode, type.props) as T; } -function trackKeysOfValidatedTypes( - type: ParsableType | t.Any, - prefix: string = '' -): t.Any { +function trackKeysOfValidatedTypes(type: ParsableType | t.Any, prefix: string = ''): t.Any { if (!('_tag' in type)) { return type; } @@ -89,27 +72,24 @@ function trackKeysOfValidatedTypes( case 'IntersectionType': { const collectionType = type as t.IntersectionType; return t.intersection( - collectionType.types.map((rt) => - trackKeysOfValidatedTypes(rt, prefix) - ) as [t.Any, t.Any] + collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any] ); } case 'UnionType': { const collectionType = type as t.UnionType; return t.union( - collectionType.types.map((rt) => - trackKeysOfValidatedTypes(rt, prefix) - ) as [t.Any, t.Any] + collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any] ); } case 'MergeType': { - const collectionType = type as MergeType; - return merge( - collectionType.types.map((rt) => - trackKeysOfValidatedTypes(rt, prefix) - ) as [t.Any, t.Any] + const collectionType = type as MergeType; + return mergeRt( + ...(collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [ + t.Any, + t.Any + ]) ); } @@ -142,9 +122,7 @@ function trackKeysOfValidatedTypes( case 'ExactType': { const exactType = type as t.ExactType; - return t.exact( - trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps - ); + return t.exact(trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps); } default: @@ -169,17 +147,11 @@ class StrictKeysType< (input, context) => { this.trackedKeys.length = 0; return either.chain(trackedType.validate(input, context), (i) => { - const originalKeys = getKeysInObject( - input as Record - ); + const originalKeys = getKeysInObject(input as Record); const excessKeys = difference(originalKeys, this.trackedKeys); if (excessKeys.length) { - return t.failure( - i, - context, - `Excess keys are not allowed: \n${excessKeys.join('\n')}` - ); + return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`); } return t.success(i); diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json new file mode 100644 index 0000000000000..6c67518e21073 --- /dev/null +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-io-ts-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json index 9450fd39607ea..96edeccad6658 100644 --- a/packages/kbn-legacy-logging/package.json +++ b/packages/kbn-legacy-logging/package.json @@ -11,6 +11,7 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@kbn/utils": "link:../kbn-utils" + "@kbn/utils": "link:../kbn-utils", + "@kbn/config-schema": "link:../kbn-config-schema" } } diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index e1edd06a4b4a2..3ece0f6f1ee47 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -88,7 +88,7 @@ export class LegacyLoggingServer { // We set `ops.interval` to max allowed number and `ops` filter to value // that doesn't exist to avoid logging of ops at all, if turned on it will be // logged by the "legacy" Kibana. - const { value: loggingConfig } = legacyLoggingConfigSchema.validate({ + const loggingConfig = legacyLoggingConfigSchema.validate({ ...legacyLoggingConfig, events: { ...legacyLoggingConfig.events, diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts index 76d7381ee8728..0330708e746c0 100644 --- a/packages/kbn-legacy-logging/src/schema.ts +++ b/packages/kbn-legacy-logging/src/schema.ts @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import Joi from 'joi'; +import { schema } from '@kbn/config-schema'; -const HANDLED_IN_KIBANA_PLATFORM = Joi.any().description( - 'This key is handled in the new platform ONLY' -); /** * @deprecated * @@ -36,46 +33,65 @@ export interface LegacyLoggingConfig { }; } -export const legacyLoggingConfigSchema = Joi.object() - .keys({ - appenders: HANDLED_IN_KIBANA_PLATFORM, - loggers: HANDLED_IN_KIBANA_PLATFORM, - root: HANDLED_IN_KIBANA_PLATFORM, - - silent: Joi.boolean().default(false), - quiet: Joi.boolean().when('silent', { - is: true, - then: Joi.boolean().default(true).valid(true), - otherwise: Joi.boolean().default(false), +export const legacyLoggingConfigSchema = schema.object({ + silent: schema.boolean({ defaultValue: false }), + quiet: schema.conditional( + schema.siblingRef('silent'), + true, + schema.boolean({ + defaultValue: true, + validate: (quiet) => { + if (!quiet) { + return 'must be true when `silent` is true'; + } + }, + }), + schema.boolean({ defaultValue: false }) + ), + verbose: schema.conditional( + schema.siblingRef('quiet'), + true, + schema.boolean({ + defaultValue: false, + validate: (verbose) => { + if (verbose) { + return 'must be false when `quiet` is true'; + } + }, + }), + schema.boolean({ defaultValue: false }) + ), + events: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + dest: schema.string({ defaultValue: 'stdout' }), + filter: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + json: schema.conditional( + schema.siblingRef('dest'), + 'stdout', + schema.boolean({ + defaultValue: !process.stdout.isTTY, + }), + schema.boolean({ + defaultValue: true, + }) + ), + timezone: schema.maybe(schema.string()), + rotate: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + everyBytes: schema.number({ + min: 1048576, // > 1MB + max: 1073741825, // < 1GB + defaultValue: 10485760, // 10MB }), - verbose: Joi.boolean().when('quiet', { - is: true, - then: Joi.valid(false).default(false), - otherwise: Joi.boolean().default(false), + keepFiles: schema.number({ + min: 2, + max: 1024, + defaultValue: 7, }), - events: Joi.any().default({}), - dest: Joi.string().default('stdout'), - filter: Joi.any().default({}), - json: Joi.boolean().when('dest', { - is: 'stdout', - then: Joi.boolean().default(!process.stdout.isTTY), - otherwise: Joi.boolean().default(true), + pollingInterval: schema.number({ + min: 5000, + max: 3600000, + defaultValue: 10000, }), - timezone: Joi.string(), - rotate: Joi.object() - .keys({ - enabled: Joi.boolean().default(false), - everyBytes: Joi.number() - // > 1MB - .greater(1048576) - // < 1GB - .less(1073741825) - // 10MB - .default(10485760), - keepFiles: Joi.number().greater(2).less(1024).default(7), - pollingInterval: Joi.number().greater(5000).less(3600000).default(10000), - usePolling: Joi.boolean().default(false), - }) - .default(), - }) - .default(); + usePolling: schema.boolean({ defaultValue: false }), + }), +}); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3c9fd4f59a406..249183d4b1e31 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,12 +46,11 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 202261 + lists: 228500 logstash: 53548 management: 46112 maps: 80000 mapsLegacy: 87859 - mapsLegacyLicensing: 20214 ml: 82187 monitoring: 80000 navigation: 37269 @@ -69,7 +68,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 189428 securityOss: 30806 - securitySolution: 283440 + securitySolution: 235402 share: 99061 snapshotRestore: 79032 spaces: 387915 diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index c546a0c6cf992..8becc76a23ca2 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -457,7 +457,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 22, + 25, ], "results": Array [ Object { @@ -480,7 +480,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 25, + 28, ], "results": Array [ Object { @@ -505,7 +505,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 23, + 26, ], "results": Array [ Object { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 509ce89f8c02c..7c5d0390d9fba 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -14455,6 +14455,7 @@ module.exports = FastGlob; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; const utils = __webpack_require__(165); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); @@ -14526,6 +14527,7 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; const array = __webpack_require__(166); exports.array = array; const errno = __webpack_require__(167); @@ -14549,6 +14551,7 @@ exports.string = string; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.splitWhen = exports.flatten = void 0; function flatten(items) { return items.reduce((collection, item) => [].concat(collection, item), []); } @@ -14577,6 +14580,7 @@ exports.splitWhen = splitWhen; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEnoentCodeError = void 0; function isEnoentCodeError(error) { return error.code === 'ENOENT'; } @@ -14590,6 +14594,7 @@ exports.isEnoentCodeError = isEnoentCodeError; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDirentFromStats = void 0; class DirentFromStats { constructor(name, stats) { this.name = name; @@ -14615,6 +14620,7 @@ exports.createDirentFromStats = createDirentFromStats; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.removeLeadingDotSegment = exports.escape = exports.makeAbsolute = exports.unixify = void 0; const path = __webpack_require__(4); const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; @@ -14654,6 +14660,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(171); const micromatch = __webpack_require__(174); @@ -14670,6 +14677,14 @@ function isStaticPattern(pattern, options = {}) { } exports.isStaticPattern = isStaticPattern; function isDynamicPattern(pattern, options = {}) { + /** + * A special case with an empty string is necessary for matching patterns that start with a forward slash. + * An empty string cannot be a dynamic pattern. + * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. + */ + if (pattern === '') { + return false; + } /** * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check * filepath directly (without read directory). @@ -14744,12 +14759,23 @@ function expandBraceExpansion(pattern) { } exports.expandBraceExpansion = expandBraceExpansion; function getPatternParts(pattern, options) { - const info = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); - // See micromatch/picomatch#58 for more details - if (info.parts.length === 0) { - return [pattern]; + let { parts } = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); + /** + * The scan method returns an empty array in some cases. + * See micromatch/picomatch#58 for more details. + */ + if (parts.length === 0) { + parts = [pattern]; + } + /** + * The scan method does not return an empty part for the pattern with a forward slash. + * This is another part of micromatch/picomatch#58. + */ + if (parts[0].startsWith('/')) { + parts[0] = parts[0].slice(1); + parts.unshift(''); } - return info.parts; + return parts; } exports.getPatternParts = getPatternParts; function makeRe(pattern, options) { @@ -18963,6 +18989,7 @@ module.exports = parse; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.merge = void 0; const merge2 = __webpack_require__(146); function merge(streams) { const mergedStream = merge2(streams); @@ -18986,6 +19013,7 @@ function propagateCloseEventToSources(streams) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmpty = exports.isString = void 0; function isString(input) { return typeof input === 'string'; } @@ -20314,8 +20342,7 @@ class DeepFilter { return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions); } _filter(basePath, entry, matcher, negativeRe) { - const depth = this._getEntryLevel(basePath, entry.path); - if (this._isSkippedByDeep(depth)) { + if (this._isSkippedByDeep(basePath, entry.path)) { return false; } if (this._isSkippedSymbolicLink(entry)) { @@ -20327,22 +20354,31 @@ class DeepFilter { } return this._isSkippedByNegativePatterns(filepath, negativeRe); } - _isSkippedByDeep(entryDepth) { - return entryDepth >= this._settings.deep; - } - _isSkippedSymbolicLink(entry) { - return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); + _isSkippedByDeep(basePath, entryPath) { + /** + * Avoid unnecessary depth calculations when it doesn't matter. + */ + if (this._settings.deep === Infinity) { + return false; + } + return this._getEntryLevel(basePath, entryPath) >= this._settings.deep; } _getEntryLevel(basePath, entryPath) { - const basePathDepth = basePath.split('/').length; const entryPathDepth = entryPath.split('/').length; - return entryPathDepth - (basePath === '' ? 0 : basePathDepth); + if (basePath === '') { + return entryPathDepth; + } + const basePathDepth = basePath.split('/').length; + return entryPathDepth - basePathDepth; + } + _isSkippedSymbolicLink(entry) { + return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); } _isSkippedByPositivePatterns(entryPath, matcher) { return !this._settings.baseNameMatch && !matcher.match(entryPath); } - _isSkippedByNegativePatterns(entryPath, negativeRe) { - return !utils.pattern.matchAny(entryPath, negativeRe); + _isSkippedByNegativePatterns(entryPath, patternsRe) { + return !utils.pattern.matchAny(entryPath, patternsRe); } } exports.default = DeepFilter; @@ -20470,20 +20506,21 @@ class EntryFilter { return (entry) => this._filter(entry, positiveRe, negativeRe); } _filter(entry, positiveRe, negativeRe) { - if (this._settings.unique) { - if (this._isDuplicateEntry(entry)) { - return false; - } - this._createIndexRecord(entry); + if (this._settings.unique && this._isDuplicateEntry(entry)) { + return false; } if (this._onlyFileFilter(entry) || this._onlyDirectoryFilter(entry)) { return false; } - if (this._isSkippedByAbsoluteNegativePatterns(entry, negativeRe)) { + if (this._isSkippedByAbsoluteNegativePatterns(entry.path, negativeRe)) { return false; } const filepath = this._settings.baseNameMatch ? entry.name : entry.path; - return this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + const isMatched = this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + if (this._settings.unique && isMatched) { + this._createIndexRecord(entry); + } + return isMatched; } _isDuplicateEntry(entry) { return this.index.has(entry.path); @@ -20497,12 +20534,12 @@ class EntryFilter { _onlyDirectoryFilter(entry) { return this._settings.onlyDirectories && !entry.dirent.isDirectory(); } - _isSkippedByAbsoluteNegativePatterns(entry, negativeRe) { + _isSkippedByAbsoluteNegativePatterns(entryPath, patternsRe) { if (!this._settings.absolute) { return false; } - const fullpath = utils.path.makeAbsolute(this._settings.cwd, entry.path); - return this._isMatchToPatterns(fullpath, negativeRe); + const fullpath = utils.path.makeAbsolute(this._settings.cwd, entryPath); + return utils.pattern.matchAny(fullpath, patternsRe); } _isMatchToPatterns(entryPath, patternsRe) { const filepath = utils.path.removeLeadingDotSegment(entryPath); @@ -20692,9 +20729,14 @@ exports.default = ReaderSync; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0; const fs = __webpack_require__(134); const os = __webpack_require__(121); -const CPU_COUNT = os.cpus().length; +/** + * The `os.cpus` method can return zero. We expect the number of cores to be greater than zero. + * https://github.com/nodejs/node/blob/7faeddf23a98c53896f8b574a6e66589e8fb1eb8/lib/os.js#L106-L107 + */ +const CPU_COUNT = Math.max(os.cpus().length, 1); exports.DEFAULT_FILE_SYSTEM_ADAPTER = { lstat: fs.lstat, lstatSync: fs.lstatSync, @@ -63636,7 +63678,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(783); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(812); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63662,7 +63704,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(783); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(812); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); @@ -90264,131 +90306,184 @@ module.exports = CpyError; "use strict"; -const arrayUnion = __webpack_require__(775); -const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(572); -const dirGlob = __webpack_require__(776); -const gitignore = __webpack_require__(780); +const fs = __webpack_require__(134); +const arrayUnion = __webpack_require__(145); +const merge2 = __webpack_require__(146); +const fastGlob = __webpack_require__(775); +const dirGlob = __webpack_require__(232); +const gitignore = __webpack_require__(810); +const {FilterStream, UniqueStream} = __webpack_require__(811); const DEFAULT_FILTER = () => false; const isNegative = pattern => pattern[0] === '!'; const assertPatternsInput = patterns => { - if (!patterns.every(x => typeof x === 'string')) { + if (!patterns.every(pattern => typeof pattern === 'string')) { throw new TypeError('Patterns must be a string or an array of strings'); } }; -const generateGlobTasks = (patterns, taskOpts) => { - patterns = [].concat(patterns); +const checkCwdOption = (options = {}) => { + if (!options.cwd) { + return; + } + + let stat; + try { + stat = fs.statSync(options.cwd); + } catch { + return; + } + + if (!stat.isDirectory()) { + throw new Error('The `cwd` option must be a path to a directory'); + } +}; + +const getPathString = p => p.stats instanceof fs.Stats ? p.path : p; + +const generateGlobTasks = (patterns, taskOptions) => { + patterns = arrayUnion([].concat(patterns)); assertPatternsInput(patterns); + checkCwdOption(taskOptions); const globTasks = []; - taskOpts = Object.assign({ + taskOptions = { ignore: [], - expandDirectories: true - }, taskOpts); + expandDirectories: true, + ...taskOptions + }; - patterns.forEach((pattern, i) => { + for (const [index, pattern] of patterns.entries()) { if (isNegative(pattern)) { - return; + continue; } const ignore = patterns - .slice(i) - .filter(isNegative) + .slice(index) + .filter(pattern => isNegative(pattern)) .map(pattern => pattern.slice(1)); - const opts = Object.assign({}, taskOpts, { - ignore: taskOpts.ignore.concat(ignore) - }); + const options = { + ...taskOptions, + ignore: taskOptions.ignore.concat(ignore) + }; - globTasks.push({pattern, opts}); - }); + globTasks.push({pattern, options}); + } return globTasks; }; const globDirs = (task, fn) => { - let opts = {cwd: task.opts.cwd}; + let options = {}; + if (task.options.cwd) { + options.cwd = task.options.cwd; + } - if (Array.isArray(task.opts.expandDirectories)) { - opts = Object.assign(opts, {files: task.opts.expandDirectories}); - } else if (typeof task.opts.expandDirectories === 'object') { - opts = Object.assign(opts, task.opts.expandDirectories); + if (Array.isArray(task.options.expandDirectories)) { + options = { + ...options, + files: task.options.expandDirectories + }; + } else if (typeof task.options.expandDirectories === 'object') { + options = { + ...options, + ...task.options.expandDirectories + }; } - return fn(task.pattern, opts); + return fn(task.pattern, options); }; -const getPattern = (task, fn) => task.opts.expandDirectories ? globDirs(task, fn) : [task.pattern]; +const getPattern = (task, fn) => task.options.expandDirectories ? globDirs(task, fn) : [task.pattern]; -module.exports = (patterns, opts) => { - let globTasks; +const getFilterSync = options => { + return options && options.gitignore ? + gitignore.sync({cwd: options.cwd, ignore: options.ignore}) : + DEFAULT_FILTER; +}; - try { - globTasks = generateGlobTasks(patterns, opts); - } catch (err) { - return Promise.reject(err); +const globToTask = task => glob => { + const {options} = task; + if (options.ignore && Array.isArray(options.ignore) && options.expandDirectories) { + options.ignore = dirGlob.sync(options.ignore); } - const getTasks = Promise.all(globTasks.map(task => Promise.resolve(getPattern(task, dirGlob)) - .then(globs => Promise.all(globs.map(glob => ({ - pattern: glob, - opts: task.opts - })))) - )) - .then(tasks => arrayUnion.apply(null, tasks)); - - const getFilter = () => { - return Promise.resolve( - opts && opts.gitignore ? - gitignore({cwd: opts.cwd, ignore: opts.ignore}) : - DEFAULT_FILTER - ); + return { + pattern: glob, + options }; - - return getFilter() - .then(filter => { - return getTasks - .then(tasks => Promise.all(tasks.map(task => fastGlob(task.pattern, task.opts)))) - .then(paths => arrayUnion.apply(null, paths)) - .then(paths => paths.filter(p => !filter(p))); - }); }; -module.exports.sync = (patterns, opts) => { - const globTasks = generateGlobTasks(patterns, opts); +module.exports = async (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); - const getFilter = () => { - return opts && opts.gitignore ? - gitignore.sync({cwd: opts.cwd, ignore: opts.ignore}) : + const getFilter = async () => { + return options && options.gitignore ? + gitignore({cwd: options.cwd, ignore: options.ignore}) : DEFAULT_FILTER; }; - const tasks = globTasks.reduce((tasks, task) => { - const newTask = getPattern(task, dirGlob.sync).map(glob => ({ - pattern: glob, - opts: task.opts + const getTasks = async () => { + const tasks = await Promise.all(globTasks.map(async task => { + const globs = await getPattern(task, dirGlob); + return Promise.all(globs.map(globToTask(task))); })); - return tasks.concat(newTask); - }, []); - const filter = getFilter(); + return arrayUnion(...tasks); + }; - return tasks.reduce( - (matches, task) => arrayUnion(matches, fastGlob.sync(task.pattern, task.opts)), - [] - ).filter(p => !filter(p)); + const [filter, tasks] = await Promise.all([getFilter(), getTasks()]); + const paths = await Promise.all(tasks.map(task => fastGlob(task.pattern, task.options))); + + return arrayUnion(...paths).filter(path_ => !filter(getPathString(path_))); +}; + +module.exports.sync = (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); + + const tasks = []; + for (const task of globTasks) { + const newTask = getPattern(task, dirGlob.sync).map(globToTask(task)); + tasks.push(...newTask); + } + + const filter = getFilterSync(options); + + let matches = []; + for (const task of tasks) { + matches = arrayUnion(matches, fastGlob.sync(task.pattern, task.options)); + } + + return matches.filter(path_ => !filter(path_)); +}; + +module.exports.stream = (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); + + const tasks = []; + for (const task of globTasks) { + const newTask = getPattern(task, dirGlob.sync).map(globToTask(task)); + tasks.push(...newTask); + } + + const filter = getFilterSync(options); + const filterStream = new FilterStream(p => !filter(p)); + const uniqueStream = new UniqueStream(); + + return merge2(tasks.map(task => fastGlob.stream(task.pattern, task.options))) + .pipe(filterStream) + .pipe(uniqueStream); }; module.exports.generateGlobTasks = generateGlobTasks; -module.exports.hasMagic = (patterns, opts) => [] +module.exports.hasMagic = (patterns, options) => [] .concat(patterns) - .some(pattern => glob.hasMagic(pattern, opts)); + .some(pattern => fastGlob.isDynamicPattern(pattern, options)); module.exports.gitignore = gitignore; @@ -90398,12 +90493,73 @@ module.exports.gitignore = gitignore; /***/ (function(module, exports, __webpack_require__) { "use strict"; - -var arrayUniq = __webpack_require__(571); - -module.exports = function () { - return arrayUniq([].concat.apply([], arguments)); -}; + +const taskManager = __webpack_require__(776); +const async_1 = __webpack_require__(796); +const stream_1 = __webpack_require__(806); +const sync_1 = __webpack_require__(807); +const settings_1 = __webpack_require__(809); +const utils = __webpack_require__(777); +async function FastGlob(source, options) { + assertPatternsInput(source); + const works = getWorks(source, async_1.default, options); + const result = await Promise.all(works); + return utils.array.flatten(result); +} +// https://github.com/typescript-eslint/typescript-eslint/issues/60 +// eslint-disable-next-line no-redeclare +(function (FastGlob) { + function sync(source, options) { + assertPatternsInput(source); + const works = getWorks(source, sync_1.default, options); + return utils.array.flatten(works); + } + FastGlob.sync = sync; + function stream(source, options) { + assertPatternsInput(source); + const works = getWorks(source, stream_1.default, options); + /** + * The stream returned by the provider cannot work with an asynchronous iterator. + * To support asynchronous iterators, regardless of the number of tasks, we always multiplex streams. + * This affects performance (+25%). I don't see best solution right now. + */ + return utils.stream.merge(works); + } + FastGlob.stream = stream; + function generateTasks(source, options) { + assertPatternsInput(source); + const patterns = [].concat(source); + const settings = new settings_1.default(options); + return taskManager.generate(patterns, settings); + } + FastGlob.generateTasks = generateTasks; + function isDynamicPattern(source, options) { + assertPatternsInput(source); + const settings = new settings_1.default(options); + return utils.pattern.isDynamicPattern(source, settings); + } + FastGlob.isDynamicPattern = isDynamicPattern; + function escapePath(source) { + assertPatternsInput(source); + return utils.path.escape(source); + } + FastGlob.escapePath = escapePath; +})(FastGlob || (FastGlob = {})); +function getWorks(source, _Provider, options) { + const patterns = [].concat(source); + const settings = new settings_1.default(options); + const tasks = taskManager.generate(patterns, settings); + const provider = new _Provider(settings); + return tasks.map(provider.read, provider); +} +function assertPatternsInput(input) { + const source = [].concat(input); + const isValidSource = source.every((item) => utils.string.isString(item) && !utils.string.isEmpty(item)); + if (!isValidSource) { + throw new TypeError('Patterns must be a string (non empty) or an array of strings'); + } +} +module.exports = FastGlob; /***/ }), @@ -90411,54 +90567,71 @@ module.exports = function () { /***/ (function(module, exports, __webpack_require__) { "use strict"; - -const path = __webpack_require__(4); -const arrify = __webpack_require__(777); -const pathType = __webpack_require__(778); - -const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; -const getPath = filepath => filepath[0] === '!' ? filepath.slice(1) : filepath; - -const addExtensions = (file, extensions) => { - if (path.extname(file)) { - return `**/${file}`; - } - - return `**/${file}.${getExtensions(extensions)}`; -}; - -const getGlob = (dir, opts) => { - opts = Object.assign({}, opts); - - if (opts.files && !Array.isArray(opts.files)) { - throw new TypeError(`\`options.files\` must be an \`Array\`, not \`${typeof opts.files}\``); - } - - if (opts.extensions && !Array.isArray(opts.extensions)) { - throw new TypeError(`\`options.extensions\` must be an \`Array\`, not \`${typeof opts.extensions}\``); - } - - if (opts.files && opts.extensions) { - return opts.files.map(x => path.join(dir, addExtensions(x, opts.extensions))); - } else if (opts.files) { - return opts.files.map(x => path.join(dir, `**/${x}`)); - } else if (opts.extensions) { - return [path.join(dir, `**/*.${getExtensions(opts.extensions)}`)]; - } - - return [path.join(dir, '**')]; -}; - -module.exports = (input, opts) => { - return Promise.all(arrify(input).map(x => pathType.dir(getPath(x)) - .then(isDir => isDir ? getGlob(x, opts) : x))) - .then(globs => [].concat.apply([], globs)); -}; - -module.exports.sync = (input, opts) => { - const globs = arrify(input).map(x => pathType.dirSync(getPath(x)) ? getGlob(x, opts) : x); - return [].concat.apply([], globs); -}; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; +const utils = __webpack_require__(777); +function generate(patterns, settings) { + const positivePatterns = getPositivePatterns(patterns); + const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); + const staticPatterns = positivePatterns.filter((pattern) => utils.pattern.isStaticPattern(pattern, settings)); + const dynamicPatterns = positivePatterns.filter((pattern) => utils.pattern.isDynamicPattern(pattern, settings)); + const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, /* dynamic */ false); + const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, /* dynamic */ true); + return staticTasks.concat(dynamicTasks); +} +exports.generate = generate; +function convertPatternsToTasks(positive, negative, dynamic) { + const positivePatternsGroup = groupPatternsByBaseDirectory(positive); + // When we have a global group – there is no reason to divide the patterns into independent tasks. + // In this case, the global task covers the rest. + if ('.' in positivePatternsGroup) { + const task = convertPatternGroupToTask('.', positive, negative, dynamic); + return [task]; + } + return convertPatternGroupsToTasks(positivePatternsGroup, negative, dynamic); +} +exports.convertPatternsToTasks = convertPatternsToTasks; +function getPositivePatterns(patterns) { + return utils.pattern.getPositivePatterns(patterns); +} +exports.getPositivePatterns = getPositivePatterns; +function getNegativePatternsAsPositive(patterns, ignore) { + const negative = utils.pattern.getNegativePatterns(patterns).concat(ignore); + const positive = negative.map(utils.pattern.convertToPositivePattern); + return positive; +} +exports.getNegativePatternsAsPositive = getNegativePatternsAsPositive; +function groupPatternsByBaseDirectory(patterns) { + const group = {}; + return patterns.reduce((collection, pattern) => { + const base = utils.pattern.getBaseDirectory(pattern); + if (base in collection) { + collection[base].push(pattern); + } + else { + collection[base] = [pattern]; + } + return collection; + }, group); +} +exports.groupPatternsByBaseDirectory = groupPatternsByBaseDirectory; +function convertPatternGroupsToTasks(positive, negative, dynamic) { + return Object.keys(positive).map((base) => { + return convertPatternGroupToTask(base, positive[base], negative, dynamic); + }); +} +exports.convertPatternGroupsToTasks = convertPatternGroupsToTasks; +function convertPatternGroupToTask(base, positive, negative, dynamic) { + return { + dynamic, + positive, + negative, + base, + patterns: [].concat(positive, negative.map(utils.pattern.convertToNegativePattern)) + }; +} +exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), @@ -90466,14 +90639,23 @@ module.exports.sync = (input, opts) => { /***/ (function(module, exports, __webpack_require__) { "use strict"; - -module.exports = function (val) { - if (val === null || val === undefined) { - return []; - } - - return Array.isArray(val) ? val : [val]; -}; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; +const array = __webpack_require__(778); +exports.array = array; +const errno = __webpack_require__(779); +exports.errno = errno; +const fs = __webpack_require__(780); +exports.fs = fs; +const path = __webpack_require__(781); +exports.path = path; +const pattern = __webpack_require__(782); +exports.pattern = pattern; +const stream = __webpack_require__(794); +exports.stream = stream; +const string = __webpack_require__(795); +exports.string = string; /***/ }), @@ -90481,204 +90663,3005 @@ module.exports = function (val) { /***/ (function(module, exports, __webpack_require__) { "use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.splitWhen = exports.flatten = void 0; +function flatten(items) { + return items.reduce((collection, item) => [].concat(collection, item), []); +} +exports.flatten = flatten; +function splitWhen(items, predicate) { + const result = [[]]; + let groupIndex = 0; + for (const item of items) { + if (predicate(item)) { + groupIndex++; + result[groupIndex] = []; + } + else { + result[groupIndex].push(item); + } + } + return result; +} +exports.splitWhen = splitWhen; -const fs = __webpack_require__(134); -const pify = __webpack_require__(779); - -function type(fn, fn2, fp) { - if (typeof fp !== 'string') { - return Promise.reject(new TypeError(`Expected a string, got ${typeof fp}`)); - } - - return pify(fs[fn])(fp) - .then(stats => stats[fn2]()) - .catch(err => { - if (err.code === 'ENOENT') { - return false; - } - - throw err; - }); -} - -function typeSync(fn, fn2, fp) { - if (typeof fp !== 'string') { - throw new TypeError(`Expected a string, got ${typeof fp}`); - } - - try { - return fs[fn](fp)[fn2](); - } catch (err) { - if (err.code === 'ENOENT') { - return false; - } - throw err; - } -} +/***/ }), +/* 779 */ +/***/ (function(module, exports, __webpack_require__) { -exports.file = type.bind(null, 'stat', 'isFile'); -exports.dir = type.bind(null, 'stat', 'isDirectory'); -exports.symlink = type.bind(null, 'lstat', 'isSymbolicLink'); -exports.fileSync = typeSync.bind(null, 'statSync', 'isFile'); -exports.dirSync = typeSync.bind(null, 'statSync', 'isDirectory'); -exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEnoentCodeError = void 0; +function isEnoentCodeError(error) { + return error.code === 'ENOENT'; +} +exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDirentFromStats = void 0; +class DirentFromStats { + constructor(name, stats) { + this.name = name; + this.isBlockDevice = stats.isBlockDevice.bind(stats); + this.isCharacterDevice = stats.isCharacterDevice.bind(stats); + this.isDirectory = stats.isDirectory.bind(stats); + this.isFIFO = stats.isFIFO.bind(stats); + this.isFile = stats.isFile.bind(stats); + this.isSocket = stats.isSocket.bind(stats); + this.isSymbolicLink = stats.isSymbolicLink.bind(stats); + } +} +function createDirentFromStats(name, stats) { + return new DirentFromStats(name, stats); +} +exports.createDirentFromStats = createDirentFromStats; -const processFn = (fn, opts) => function () { - const P = opts.promiseModule; - const args = new Array(arguments.length); +/***/ }), +/* 781 */ +/***/ (function(module, exports, __webpack_require__) { - for (let i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.removeLeadingDotSegment = exports.escape = exports.makeAbsolute = exports.unixify = void 0; +const path = __webpack_require__(4); +const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ +const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; +/** + * Designed to work only with simple paths: `dir\\file`. + */ +function unixify(filepath) { + return filepath.replace(/\\/g, '/'); +} +exports.unixify = unixify; +function makeAbsolute(cwd, filepath) { + return path.resolve(cwd, filepath); +} +exports.makeAbsolute = makeAbsolute; +function escape(pattern) { + return pattern.replace(UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); +} +exports.escape = escape; +function removeLeadingDotSegment(entry) { + // We do not use `startsWith` because this is 10x slower than current implementation for some cases. + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + if (entry.charAt(0) === '.') { + const secondCharactery = entry.charAt(1); + if (secondCharactery === '/' || secondCharactery === '\\') { + return entry.slice(LEADING_DOT_SEGMENT_CHARACTERS_COUNT); + } + } + return entry; +} +exports.removeLeadingDotSegment = removeLeadingDotSegment; - return new P((resolve, reject) => { - if (opts.errorFirst) { - args.push(function (err, result) { - if (opts.multiArgs) { - const results = new Array(arguments.length - 1); - for (let i = 1; i < arguments.length; i++) { - results[i - 1] = arguments[i]; - } +/***/ }), +/* 782 */ +/***/ (function(module, exports, __webpack_require__) { - if (err) { - results.unshift(err); - reject(results); - } else { - resolve(results); - } - } else if (err) { - reject(err); - } else { - resolve(result); - } - }); - } else { - args.push(function (result) { - if (opts.multiArgs) { - const results = new Array(arguments.length - 1); +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; +const path = __webpack_require__(4); +const globParent = __webpack_require__(171); +const micromatch = __webpack_require__(783); +const picomatch = __webpack_require__(185); +const GLOBSTAR = '**'; +const ESCAPE_SYMBOL = '\\'; +const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; +const REGEX_CHARACTER_CLASS_SYMBOLS_RE = /\[.*]/; +const REGEX_GROUP_SYMBOLS_RE = /(?:^|[^!*+?@])\(.*\|.*\)/; +const GLOB_EXTENSION_SYMBOLS_RE = /[!*+?@]\(.*\)/; +const BRACE_EXPANSIONS_SYMBOLS_RE = /{.*(?:,|\.\.).*}/; +function isStaticPattern(pattern, options = {}) { + return !isDynamicPattern(pattern, options); +} +exports.isStaticPattern = isStaticPattern; +function isDynamicPattern(pattern, options = {}) { + /** + * A special case with an empty string is necessary for matching patterns that start with a forward slash. + * An empty string cannot be a dynamic pattern. + * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. + */ + if (pattern === '') { + return false; + } + /** + * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check + * filepath directly (without read directory). + */ + if (options.caseSensitiveMatch === false || pattern.includes(ESCAPE_SYMBOL)) { + return true; + } + if (COMMON_GLOB_SYMBOLS_RE.test(pattern) || REGEX_CHARACTER_CLASS_SYMBOLS_RE.test(pattern) || REGEX_GROUP_SYMBOLS_RE.test(pattern)) { + return true; + } + if (options.extglob !== false && GLOB_EXTENSION_SYMBOLS_RE.test(pattern)) { + return true; + } + if (options.braceExpansion !== false && BRACE_EXPANSIONS_SYMBOLS_RE.test(pattern)) { + return true; + } + return false; +} +exports.isDynamicPattern = isDynamicPattern; +function convertToPositivePattern(pattern) { + return isNegativePattern(pattern) ? pattern.slice(1) : pattern; +} +exports.convertToPositivePattern = convertToPositivePattern; +function convertToNegativePattern(pattern) { + return '!' + pattern; +} +exports.convertToNegativePattern = convertToNegativePattern; +function isNegativePattern(pattern) { + return pattern.startsWith('!') && pattern[1] !== '('; +} +exports.isNegativePattern = isNegativePattern; +function isPositivePattern(pattern) { + return !isNegativePattern(pattern); +} +exports.isPositivePattern = isPositivePattern; +function getNegativePatterns(patterns) { + return patterns.filter(isNegativePattern); +} +exports.getNegativePatterns = getNegativePatterns; +function getPositivePatterns(patterns) { + return patterns.filter(isPositivePattern); +} +exports.getPositivePatterns = getPositivePatterns; +function getBaseDirectory(pattern) { + return globParent(pattern, { flipBackslashes: false }); +} +exports.getBaseDirectory = getBaseDirectory; +function hasGlobStar(pattern) { + return pattern.includes(GLOBSTAR); +} +exports.hasGlobStar = hasGlobStar; +function endsWithSlashGlobStar(pattern) { + return pattern.endsWith('/' + GLOBSTAR); +} +exports.endsWithSlashGlobStar = endsWithSlashGlobStar; +function isAffectDepthOfReadingPattern(pattern) { + const basename = path.basename(pattern); + return endsWithSlashGlobStar(pattern) || isStaticPattern(basename); +} +exports.isAffectDepthOfReadingPattern = isAffectDepthOfReadingPattern; +function expandPatternsWithBraceExpansion(patterns) { + return patterns.reduce((collection, pattern) => { + return collection.concat(expandBraceExpansion(pattern)); + }, []); +} +exports.expandPatternsWithBraceExpansion = expandPatternsWithBraceExpansion; +function expandBraceExpansion(pattern) { + return micromatch.braces(pattern, { + expand: true, + nodupes: true + }); +} +exports.expandBraceExpansion = expandBraceExpansion; +function getPatternParts(pattern, options) { + let { parts } = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); + /** + * The scan method returns an empty array in some cases. + * See micromatch/picomatch#58 for more details. + */ + if (parts.length === 0) { + parts = [pattern]; + } + /** + * The scan method does not return an empty part for the pattern with a forward slash. + * This is another part of micromatch/picomatch#58. + */ + if (parts[0].startsWith('/')) { + parts[0] = parts[0].slice(1); + parts.unshift(''); + } + return parts; +} +exports.getPatternParts = getPatternParts; +function makeRe(pattern, options) { + return micromatch.makeRe(pattern, options); +} +exports.makeRe = makeRe; +function convertPatternsToRe(patterns, options) { + return patterns.map((pattern) => makeRe(pattern, options)); +} +exports.convertPatternsToRe = convertPatternsToRe; +function matchAny(entry, patternsRe) { + return patternsRe.some((patternRe) => patternRe.test(entry)); +} +exports.matchAny = matchAny; + + +/***/ }), +/* 783 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const util = __webpack_require__(112); +const braces = __webpack_require__(784); +const picomatch = __webpack_require__(185); +const utils = __webpack_require__(188); +const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); + +/** + * Returns an array of strings that match one or more glob patterns. + * + * ```js + * const mm = require('micromatch'); + * // mm(list, patterns[, options]); + * + * console.log(mm(['a.js', 'a.txt'], ['*.js'])); + * //=> [ 'a.js' ] + * ``` + * @param {String|Array} list List of strings to match. + * @param {String|Array} patterns One or more glob patterns to use for matching. + * @param {Object} options See available [options](#options) + * @return {Array} Returns an array of matches + * @summary false + * @api public + */ + +const micromatch = (list, patterns, options) => { + patterns = [].concat(patterns); + list = [].concat(list); + + let omit = new Set(); + let keep = new Set(); + let items = new Set(); + let negatives = 0; + + let onResult = state => { + items.add(state.output); + if (options && options.onResult) { + options.onResult(state); + } + }; + + for (let i = 0; i < patterns.length; i++) { + let isMatch = picomatch(String(patterns[i]), { ...options, onResult }, true); + let negated = isMatch.state.negated || isMatch.state.negatedExtglob; + if (negated) negatives++; + + for (let item of list) { + let matched = isMatch(item, true); + + let match = negated ? !matched.isMatch : matched.isMatch; + if (!match) continue; + + if (negated) { + omit.add(matched.output); + } else { + omit.delete(matched.output); + keep.add(matched.output); + } + } + } + + let result = negatives === patterns.length ? [...items] : [...keep]; + let matches = result.filter(item => !omit.has(item)); + + if (options && matches.length === 0) { + if (options.failglob === true) { + throw new Error(`No matches found for "${patterns.join(', ')}"`); + } + + if (options.nonull === true || options.nullglob === true) { + return options.unescape ? patterns.map(p => p.replace(/\\/g, '')) : patterns; + } + } + + return matches; +}; + +/** + * Backwards compatibility + */ + +micromatch.match = micromatch; + +/** + * Returns a matcher function from the given glob `pattern` and `options`. + * The returned function takes a string to match as its only argument and returns + * true if the string is a match. + * + * ```js + * const mm = require('micromatch'); + * // mm.matcher(pattern[, options]); + * + * const isMatch = mm.matcher('*.!(*a)'); + * console.log(isMatch('a.a')); //=> false + * console.log(isMatch('a.b')); //=> true + * ``` + * @param {String} `pattern` Glob pattern + * @param {Object} `options` + * @return {Function} Returns a matcher function. + * @api public + */ + +micromatch.matcher = (pattern, options) => picomatch(pattern, options); + +/** + * Returns true if **any** of the given glob `patterns` match the specified `string`. + * + * ```js + * const mm = require('micromatch'); + * // mm.isMatch(string, patterns[, options]); + * + * console.log(mm.isMatch('a.a', ['b.*', '*.a'])); //=> true + * console.log(mm.isMatch('a.a', 'b.*')); //=> false + * ``` + * @param {String} str The string to test. + * @param {String|Array} patterns One or more glob patterns to use for matching. + * @param {Object} [options] See available [options](#options). + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str); + +/** + * Backwards compatibility + */ + +micromatch.any = micromatch.isMatch; + +/** + * Returns a list of strings that _**do not match any**_ of the given `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.not(list, patterns[, options]); + * + * console.log(mm.not(['a.a', 'b.b', 'c.c'], '*.a')); + * //=> ['b.b', 'c.c'] + * ``` + * @param {Array} `list` Array of strings to match. + * @param {String|Array} `patterns` One or more glob pattern to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Array} Returns an array of strings that **do not match** the given patterns. + * @api public + */ + +micromatch.not = (list, patterns, options = {}) => { + patterns = [].concat(patterns).map(String); + let result = new Set(); + let items = []; + + let onResult = state => { + if (options.onResult) options.onResult(state); + items.push(state.output); + }; + + let matches = micromatch(list, patterns, { ...options, onResult }); + + for (let item of items) { + if (!matches.includes(item)) { + result.add(item); + } + } + return [...result]; +}; + +/** + * Returns true if the given `string` contains the given pattern. Similar + * to [.isMatch](#isMatch) but the pattern can match any part of the string. + * + * ```js + * var mm = require('micromatch'); + * // mm.contains(string, pattern[, options]); + * + * console.log(mm.contains('aa/bb/cc', '*b')); + * //=> true + * console.log(mm.contains('aa/bb/cc', '*d')); + * //=> false + * ``` + * @param {String} `str` The string to match. + * @param {String|Array} `patterns` Glob pattern to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if the patter matches any part of `str`. + * @api public + */ + +micromatch.contains = (str, pattern, options) => { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string: "${util.inspect(str)}"`); + } + + if (Array.isArray(pattern)) { + return pattern.some(p => micromatch.contains(str, p, options)); + } + + if (typeof pattern === 'string') { + if (isEmptyString(str) || isEmptyString(pattern)) { + return false; + } + + if (str.includes(pattern) || (str.startsWith('./') && str.slice(2).includes(pattern))) { + return true; + } + } + + return micromatch.isMatch(str, pattern, { ...options, contains: true }); +}; + +/** + * Filter the keys of the given object with the given `glob` pattern + * and `options`. Does not attempt to match nested keys. If you need this feature, + * use [glob-object][] instead. + * + * ```js + * const mm = require('micromatch'); + * // mm.matchKeys(object, patterns[, options]); + * + * const obj = { aa: 'a', ab: 'b', ac: 'c' }; + * console.log(mm.matchKeys(obj, '*b')); + * //=> { ab: 'b' } + * ``` + * @param {Object} `object` The object with keys to filter. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Object} Returns an object with only keys that match the given patterns. + * @api public + */ + +micromatch.matchKeys = (obj, patterns, options) => { + if (!utils.isObject(obj)) { + throw new TypeError('Expected the first argument to be an object'); + } + let keys = micromatch(Object.keys(obj), patterns, options); + let res = {}; + for (let key of keys) res[key] = obj[key]; + return res; +}; + +/** + * Returns true if some of the strings in the given `list` match any of the given glob `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.some(list, patterns[, options]); + * + * console.log(mm.some(['foo.js', 'bar.js'], ['*.js', '!foo.js'])); + * // true + * console.log(mm.some(['foo.js'], ['*.js', '!foo.js'])); + * // false + * ``` + * @param {String|Array} `list` The string or array of strings to test. Returns as soon as the first match is found. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.some = (list, patterns, options) => { + let items = [].concat(list); + + for (let pattern of [].concat(patterns)) { + let isMatch = picomatch(String(pattern), options); + if (items.some(item => isMatch(item))) { + return true; + } + } + return false; +}; + +/** + * Returns true if every string in the given `list` matches + * any of the given glob `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.every(list, patterns[, options]); + * + * console.log(mm.every('foo.js', ['foo.js'])); + * // true + * console.log(mm.every(['foo.js', 'bar.js'], ['*.js'])); + * // true + * console.log(mm.every(['foo.js', 'bar.js'], ['*.js', '!foo.js'])); + * // false + * console.log(mm.every(['foo.js'], ['*.js', '!foo.js'])); + * // false + * ``` + * @param {String|Array} `list` The string or array of strings to test. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.every = (list, patterns, options) => { + let items = [].concat(list); + + for (let pattern of [].concat(patterns)) { + let isMatch = picomatch(String(pattern), options); + if (!items.every(item => isMatch(item))) { + return false; + } + } + return true; +}; + +/** + * Returns true if **all** of the given `patterns` match + * the specified string. + * + * ```js + * const mm = require('micromatch'); + * // mm.all(string, patterns[, options]); + * + * console.log(mm.all('foo.js', ['foo.js'])); + * // true + * + * console.log(mm.all('foo.js', ['*.js', '!foo.js'])); + * // false + * + * console.log(mm.all('foo.js', ['*.js', 'foo.js'])); + * // true + * + * console.log(mm.all('foo.js', ['*.js', 'f*', '*o*', '*o.js'])); + * // true + * ``` + * @param {String|Array} `str` The string to test. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.all = (str, patterns, options) => { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string: "${util.inspect(str)}"`); + } + + return [].concat(patterns).every(p => picomatch(p, options)(str)); +}; + +/** + * Returns an array of matches captured by `pattern` in `string, or `null` if the pattern did not match. + * + * ```js + * const mm = require('micromatch'); + * // mm.capture(pattern, string[, options]); + * + * console.log(mm.capture('test/*.js', 'test/foo.js')); + * //=> ['foo'] + * console.log(mm.capture('test/*.js', 'foo/bar.css')); + * //=> null + * ``` + * @param {String} `glob` Glob pattern to use for matching. + * @param {String} `input` String to match + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns an array of captures if the input matches the glob pattern, otherwise `null`. + * @api public + */ + +micromatch.capture = (glob, input, options) => { + let posix = utils.isWindows(options); + let regex = picomatch.makeRe(String(glob), { ...options, capture: true }); + let match = regex.exec(posix ? utils.toPosixSlashes(input) : input); + + if (match) { + return match.slice(1).map(v => v === void 0 ? '' : v); + } +}; + +/** + * Create a regular expression from the given glob `pattern`. + * + * ```js + * const mm = require('micromatch'); + * // mm.makeRe(pattern[, options]); + * + * console.log(mm.makeRe('*.js')); + * //=> /^(?:(\.[\\\/])?(?!\.)(?=.)[^\/]*?\.js)$/ + * ``` + * @param {String} `pattern` A glob pattern to convert to regex. + * @param {Object} `options` + * @return {RegExp} Returns a regex created from the given pattern. + * @api public + */ + +micromatch.makeRe = (...args) => picomatch.makeRe(...args); + +/** + * Scan a glob pattern to separate the pattern into segments. Used + * by the [split](#split) method. + * + * ```js + * const mm = require('micromatch'); + * const state = mm.scan(pattern[, options]); + * ``` + * @param {String} `pattern` + * @param {Object} `options` + * @return {Object} Returns an object with + * @api public + */ + +micromatch.scan = (...args) => picomatch.scan(...args); + +/** + * Parse a glob pattern to create the source string for a regular + * expression. + * + * ```js + * const mm = require('micromatch'); + * const state = mm(pattern[, options]); + * ``` + * @param {String} `glob` + * @param {Object} `options` + * @return {Object} Returns an object with useful properties and output to be used as regex source string. + * @api public + */ + +micromatch.parse = (patterns, options) => { + let res = []; + for (let pattern of [].concat(patterns || [])) { + for (let str of braces(String(pattern), options)) { + res.push(picomatch.parse(str, options)); + } + } + return res; +}; + +/** + * Process the given brace `pattern`. + * + * ```js + * const { braces } = require('micromatch'); + * console.log(braces('foo/{a,b,c}/bar')); + * //=> [ 'foo/(a|b|c)/bar' ] + * + * console.log(braces('foo/{a,b,c}/bar', { expand: true })); + * //=> [ 'foo/a/bar', 'foo/b/bar', 'foo/c/bar' ] + * ``` + * @param {String} `pattern` String with brace pattern to process. + * @param {Object} `options` Any [options](#options) to change how expansion is performed. See the [braces][] library for all available options. + * @return {Array} + * @api public + */ + +micromatch.braces = (pattern, options) => { + if (typeof pattern !== 'string') throw new TypeError('Expected a string'); + if ((options && options.nobrace === true) || !/\{.*\}/.test(pattern)) { + return [pattern]; + } + return braces(pattern, options); +}; + +/** + * Expand braces + */ + +micromatch.braceExpand = (pattern, options) => { + if (typeof pattern !== 'string') throw new TypeError('Expected a string'); + return micromatch.braces(pattern, { ...options, expand: true }); +}; + +/** + * Expose micromatch + */ + +module.exports = micromatch; + + +/***/ }), +/* 784 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const stringify = __webpack_require__(785); +const compile = __webpack_require__(787); +const expand = __webpack_require__(791); +const parse = __webpack_require__(792); + +/** + * Expand the given pattern or create a regex-compatible string. + * + * ```js + * const braces = require('braces'); + * console.log(braces('{a,b,c}', { compile: true })); //=> ['(a|b|c)'] + * console.log(braces('{a,b,c}')); //=> ['a', 'b', 'c'] + * ``` + * @param {String} `str` + * @param {Object} `options` + * @return {String} + * @api public + */ + +const braces = (input, options = {}) => { + let output = []; + + if (Array.isArray(input)) { + for (let pattern of input) { + let result = braces.create(pattern, options); + if (Array.isArray(result)) { + output.push(...result); + } else { + output.push(result); + } + } + } else { + output = [].concat(braces.create(input, options)); + } + + if (options && options.expand === true && options.nodupes === true) { + output = [...new Set(output)]; + } + return output; +}; + +/** + * Parse the given `str` with the given `options`. + * + * ```js + * // braces.parse(pattern, [, options]); + * const ast = braces.parse('a/{b,c}/d'); + * console.log(ast); + * ``` + * @param {String} pattern Brace pattern to parse + * @param {Object} options + * @return {Object} Returns an AST + * @api public + */ + +braces.parse = (input, options = {}) => parse(input, options); + +/** + * Creates a braces string from an AST, or an AST node. + * + * ```js + * const braces = require('braces'); + * let ast = braces.parse('foo/{a,b}/bar'); + * console.log(stringify(ast.nodes[2])); //=> '{a,b}' + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.stringify = (input, options = {}) => { + if (typeof input === 'string') { + return stringify(braces.parse(input, options), options); + } + return stringify(input, options); +}; + +/** + * Compiles a brace pattern into a regex-compatible, optimized string. + * This method is called by the main [braces](#braces) function by default. + * + * ```js + * const braces = require('braces'); + * console.log(braces.compile('a/{b,c}/d')); + * //=> ['a/(b|c)/d'] + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.compile = (input, options = {}) => { + if (typeof input === 'string') { + input = braces.parse(input, options); + } + return compile(input, options); +}; + +/** + * Expands a brace pattern into an array. This method is called by the + * main [braces](#braces) function when `options.expand` is true. Before + * using this method it's recommended that you read the [performance notes](#performance)) + * and advantages of using [.compile](#compile) instead. + * + * ```js + * const braces = require('braces'); + * console.log(braces.expand('a/{b,c}/d')); + * //=> ['a/b/d', 'a/c/d']; + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.expand = (input, options = {}) => { + if (typeof input === 'string') { + input = braces.parse(input, options); + } + + let result = expand(input, options); + + // filter out empty strings if specified + if (options.noempty === true) { + result = result.filter(Boolean); + } + + // filter out duplicates if specified + if (options.nodupes === true) { + result = [...new Set(result)]; + } + + return result; +}; + +/** + * Processes a brace pattern and returns either an expanded array + * (if `options.expand` is true), a highly optimized regex-compatible string. + * This method is called by the main [braces](#braces) function. + * + * ```js + * const braces = require('braces'); + * console.log(braces.create('user-{200..300}/project-{a,b,c}-{1..10}')) + * //=> 'user-(20[0-9]|2[1-9][0-9]|300)/project-(a|b|c)-([1-9]|10)' + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.create = (input, options = {}) => { + if (input === '' || input.length < 3) { + return [input]; + } + + return options.expand !== true + ? braces.compile(input, options) + : braces.expand(input, options); +}; + +/** + * Expose "braces" + */ + +module.exports = braces; + + +/***/ }), +/* 785 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const utils = __webpack_require__(786); + +module.exports = (ast, options = {}) => { + let stringify = (node, parent = {}) => { + let invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let output = ''; + + if (node.value) { + if ((invalidBlock || invalidNode) && utils.isOpenOrClose(node)) { + return '\\' + node.value; + } + return node.value; + } + + if (node.value) { + return node.value; + } + + if (node.nodes) { + for (let child of node.nodes) { + output += stringify(child); + } + } + return output; + }; + + return stringify(ast); +}; + + + +/***/ }), +/* 786 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.isInteger = num => { + if (typeof num === 'number') { + return Number.isInteger(num); + } + if (typeof num === 'string' && num.trim() !== '') { + return Number.isInteger(Number(num)); + } + return false; +}; + +/** + * Find a node of the given type + */ + +exports.find = (node, type) => node.nodes.find(node => node.type === type); + +/** + * Find a node of the given type + */ + +exports.exceedsLimit = (min, max, step = 1, limit) => { + if (limit === false) return false; + if (!exports.isInteger(min) || !exports.isInteger(max)) return false; + return ((Number(max) - Number(min)) / Number(step)) >= limit; +}; + +/** + * Escape the given node with '\\' before node.value + */ + +exports.escapeNode = (block, n = 0, type) => { + let node = block.nodes[n]; + if (!node) return; + + if ((type && node.type === type) || node.type === 'open' || node.type === 'close') { + if (node.escaped !== true) { + node.value = '\\' + node.value; + node.escaped = true; + } + } +}; + +/** + * Returns true if the given brace node should be enclosed in literal braces + */ + +exports.encloseBrace = node => { + if (node.type !== 'brace') return false; + if ((node.commas >> 0 + node.ranges >> 0) === 0) { + node.invalid = true; + return true; + } + return false; +}; + +/** + * Returns true if a brace node is invalid. + */ + +exports.isInvalidBrace = block => { + if (block.type !== 'brace') return false; + if (block.invalid === true || block.dollar) return true; + if ((block.commas >> 0 + block.ranges >> 0) === 0) { + block.invalid = true; + return true; + } + if (block.open !== true || block.close !== true) { + block.invalid = true; + return true; + } + return false; +}; + +/** + * Returns true if a node is an open or close node + */ + +exports.isOpenOrClose = node => { + if (node.type === 'open' || node.type === 'close') { + return true; + } + return node.open === true || node.close === true; +}; + +/** + * Reduce an array of text nodes. + */ + +exports.reduce = nodes => nodes.reduce((acc, node) => { + if (node.type === 'text') acc.push(node.value); + if (node.type === 'range') node.type = 'text'; + return acc; +}, []); + +/** + * Flatten an array + */ + +exports.flatten = (...args) => { + const result = []; + const flat = arr => { + for (let i = 0; i < arr.length; i++) { + let ele = arr[i]; + Array.isArray(ele) ? flat(ele, result) : ele !== void 0 && result.push(ele); + } + return result; + }; + flat(args); + return result; +}; + + +/***/ }), +/* 787 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const fill = __webpack_require__(788); +const utils = __webpack_require__(786); + +const compile = (ast, options = {}) => { + let walk = (node, parent = {}) => { + let invalidBlock = utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let invalid = invalidBlock === true || invalidNode === true; + let prefix = options.escapeInvalid === true ? '\\' : ''; + let output = ''; + + if (node.isOpen === true) { + return prefix + node.value; + } + if (node.isClose === true) { + return prefix + node.value; + } + + if (node.type === 'open') { + return invalid ? (prefix + node.value) : '('; + } + + if (node.type === 'close') { + return invalid ? (prefix + node.value) : ')'; + } + + if (node.type === 'comma') { + return node.prev.type === 'comma' ? '' : (invalid ? node.value : '|'); + } + + if (node.value) { + return node.value; + } + + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + let range = fill(...args, { ...options, wrap: false, toRegex: true }); + + if (range.length !== 0) { + return args.length > 1 && range.length > 1 ? `(${range})` : range; + } + } + + if (node.nodes) { + for (let child of node.nodes) { + output += walk(child, node); + } + } + return output; + }; + + return walk(ast); +}; + +module.exports = compile; + + +/***/ }), +/* 788 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +const util = __webpack_require__(112); +const toRegexRange = __webpack_require__(789); + +const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); + +const transform = toNumber => { + return value => toNumber === true ? Number(value) : String(value); +}; + +const isValidValue = value => { + return typeof value === 'number' || (typeof value === 'string' && value !== ''); +}; + +const isNumber = num => Number.isInteger(+num); + +const zeros = input => { + let value = `${input}`; + let index = -1; + if (value[0] === '-') value = value.slice(1); + if (value === '0') return false; + while (value[++index] === '0'); + return index > 0; +}; + +const stringify = (start, end, options) => { + if (typeof start === 'string' || typeof end === 'string') { + return true; + } + return options.stringify === true; +}; + +const pad = (input, maxLength, toNumber) => { + if (maxLength > 0) { + let dash = input[0] === '-' ? '-' : ''; + if (dash) input = input.slice(1); + input = (dash + input.padStart(dash ? maxLength - 1 : maxLength, '0')); + } + if (toNumber === false) { + return String(input); + } + return input; +}; + +const toMaxLen = (input, maxLength) => { + let negative = input[0] === '-' ? '-' : ''; + if (negative) { + input = input.slice(1); + maxLength--; + } + while (input.length < maxLength) input = '0' + input; + return negative ? ('-' + input) : input; +}; + +const toSequence = (parts, options) => { + parts.negatives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + parts.positives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + + let prefix = options.capture ? '' : '?:'; + let positives = ''; + let negatives = ''; + let result; + + if (parts.positives.length) { + positives = parts.positives.join('|'); + } + + if (parts.negatives.length) { + negatives = `-(${prefix}${parts.negatives.join('|')})`; + } + + if (positives && negatives) { + result = `${positives}|${negatives}`; + } else { + result = positives || negatives; + } + + if (options.wrap) { + return `(${prefix}${result})`; + } + + return result; +}; + +const toRange = (a, b, isNumbers, options) => { + if (isNumbers) { + return toRegexRange(a, b, { wrap: false, ...options }); + } + + let start = String.fromCharCode(a); + if (a === b) return start; + + let stop = String.fromCharCode(b); + return `[${start}-${stop}]`; +}; + +const toRegex = (start, end, options) => { + if (Array.isArray(start)) { + let wrap = options.wrap === true; + let prefix = options.capture ? '' : '?:'; + return wrap ? `(${prefix}${start.join('|')})` : start.join('|'); + } + return toRegexRange(start, end, options); +}; + +const rangeError = (...args) => { + return new RangeError('Invalid range arguments: ' + util.inspect(...args)); +}; + +const invalidRange = (start, end, options) => { + if (options.strictRanges === true) throw rangeError([start, end]); + return []; +}; + +const invalidStep = (step, options) => { + if (options.strictRanges === true) { + throw new TypeError(`Expected step "${step}" to be a number`); + } + return []; +}; + +const fillNumbers = (start, end, step = 1, options = {}) => { + let a = Number(start); + let b = Number(end); + + if (!Number.isInteger(a) || !Number.isInteger(b)) { + if (options.strictRanges === true) throw rangeError([start, end]); + return []; + } + + // fix negative zero + if (a === 0) a = 0; + if (b === 0) b = 0; + + let descending = a > b; + let startString = String(start); + let endString = String(end); + let stepString = String(step); + step = Math.max(Math.abs(step), 1); + + let padded = zeros(startString) || zeros(endString) || zeros(stepString); + let maxLen = padded ? Math.max(startString.length, endString.length, stepString.length) : 0; + let toNumber = padded === false && stringify(start, end, options) === false; + let format = options.transform || transform(toNumber); + + if (options.toRegex && step === 1) { + return toRange(toMaxLen(start, maxLen), toMaxLen(end, maxLen), true, options); + } + + let parts = { negatives: [], positives: [] }; + let push = num => parts[num < 0 ? 'negatives' : 'positives'].push(Math.abs(num)); + let range = []; + let index = 0; + + while (descending ? a >= b : a <= b) { + if (options.toRegex === true && step > 1) { + push(a); + } else { + range.push(pad(format(a, index), maxLen, toNumber)); + } + a = descending ? a - step : a + step; + index++; + } + + if (options.toRegex === true) { + return step > 1 + ? toSequence(parts, options) + : toRegex(range, null, { wrap: false, ...options }); + } + + return range; +}; + +const fillLetters = (start, end, step = 1, options = {}) => { + if ((!isNumber(start) && start.length > 1) || (!isNumber(end) && end.length > 1)) { + return invalidRange(start, end, options); + } + + + let format = options.transform || (val => String.fromCharCode(val)); + let a = `${start}`.charCodeAt(0); + let b = `${end}`.charCodeAt(0); + + let descending = a > b; + let min = Math.min(a, b); + let max = Math.max(a, b); + + if (options.toRegex && step === 1) { + return toRange(min, max, false, options); + } + + let range = []; + let index = 0; + + while (descending ? a >= b : a <= b) { + range.push(format(a, index)); + a = descending ? a - step : a + step; + index++; + } + + if (options.toRegex === true) { + return toRegex(range, null, { wrap: false, options }); + } + + return range; +}; + +const fill = (start, end, step, options = {}) => { + if (end == null && isValidValue(start)) { + return [start]; + } + + if (!isValidValue(start) || !isValidValue(end)) { + return invalidRange(start, end, options); + } + + if (typeof step === 'function') { + return fill(start, end, 1, { transform: step }); + } + + if (isObject(step)) { + return fill(start, end, 0, step); + } + + let opts = { ...options }; + if (opts.capture === true) opts.wrap = true; + step = step || opts.step || 1; + + if (!isNumber(step)) { + if (step != null && !isObject(step)) return invalidStep(step, opts); + return fill(start, end, 1, step); + } + + if (isNumber(start) && isNumber(end)) { + return fillNumbers(start, end, step, opts); + } + + return fillLetters(start, end, Math.max(Math.abs(step), 1), opts); +}; + +module.exports = fill; + + +/***/ }), +/* 789 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ + + + +const isNumber = __webpack_require__(790); + +const toRegexRange = (min, max, options) => { + if (isNumber(min) === false) { + throw new TypeError('toRegexRange: expected the first argument to be a number'); + } + + if (max === void 0 || min === max) { + return String(min); + } + + if (isNumber(max) === false) { + throw new TypeError('toRegexRange: expected the second argument to be a number.'); + } + + let opts = { relaxZeros: true, ...options }; + if (typeof opts.strictZeros === 'boolean') { + opts.relaxZeros = opts.strictZeros === false; + } + + let relax = String(opts.relaxZeros); + let shorthand = String(opts.shorthand); + let capture = String(opts.capture); + let wrap = String(opts.wrap); + let cacheKey = min + ':' + max + '=' + relax + shorthand + capture + wrap; + + if (toRegexRange.cache.hasOwnProperty(cacheKey)) { + return toRegexRange.cache[cacheKey].result; + } + + let a = Math.min(min, max); + let b = Math.max(min, max); + + if (Math.abs(a - b) === 1) { + let result = min + '|' + max; + if (opts.capture) { + return `(${result})`; + } + if (opts.wrap === false) { + return result; + } + return `(?:${result})`; + } + + let isPadded = hasPadding(min) || hasPadding(max); + let state = { min, max, a, b }; + let positives = []; + let negatives = []; + + if (isPadded) { + state.isPadded = isPadded; + state.maxLen = String(state.max).length; + } + + if (a < 0) { + let newMin = b < 0 ? Math.abs(b) : 1; + negatives = splitToPatterns(newMin, Math.abs(a), state, opts); + a = state.a = 0; + } + + if (b >= 0) { + positives = splitToPatterns(a, b, state, opts); + } + + state.negatives = negatives; + state.positives = positives; + state.result = collatePatterns(negatives, positives, opts); + + if (opts.capture === true) { + state.result = `(${state.result})`; + } else if (opts.wrap !== false && (positives.length + negatives.length) > 1) { + state.result = `(?:${state.result})`; + } + + toRegexRange.cache[cacheKey] = state; + return state.result; +}; + +function collatePatterns(neg, pos, options) { + let onlyNegative = filterPatterns(neg, pos, '-', false, options) || []; + let onlyPositive = filterPatterns(pos, neg, '', false, options) || []; + let intersected = filterPatterns(neg, pos, '-?', true, options) || []; + let subpatterns = onlyNegative.concat(intersected).concat(onlyPositive); + return subpatterns.join('|'); +} + +function splitToRanges(min, max) { + let nines = 1; + let zeros = 1; + + let stop = countNines(min, nines); + let stops = new Set([max]); + + while (min <= stop && stop <= max) { + stops.add(stop); + nines += 1; + stop = countNines(min, nines); + } + + stop = countZeros(max + 1, zeros) - 1; + + while (min < stop && stop <= max) { + stops.add(stop); + zeros += 1; + stop = countZeros(max + 1, zeros) - 1; + } + + stops = [...stops]; + stops.sort(compare); + return stops; +} + +/** + * Convert a range to a regex pattern + * @param {Number} `start` + * @param {Number} `stop` + * @return {String} + */ + +function rangeToPattern(start, stop, options) { + if (start === stop) { + return { pattern: start, count: [], digits: 0 }; + } + + let zipped = zip(start, stop); + let digits = zipped.length; + let pattern = ''; + let count = 0; + + for (let i = 0; i < digits; i++) { + let [startDigit, stopDigit] = zipped[i]; + + if (startDigit === stopDigit) { + pattern += startDigit; + + } else if (startDigit !== '0' || stopDigit !== '9') { + pattern += toCharacterClass(startDigit, stopDigit, options); + + } else { + count++; + } + } + + if (count) { + pattern += options.shorthand === true ? '\\d' : '[0-9]'; + } + + return { pattern, count: [count], digits }; +} + +function splitToPatterns(min, max, tok, options) { + let ranges = splitToRanges(min, max); + let tokens = []; + let start = min; + let prev; + + for (let i = 0; i < ranges.length; i++) { + let max = ranges[i]; + let obj = rangeToPattern(String(start), String(max), options); + let zeros = ''; + + if (!tok.isPadded && prev && prev.pattern === obj.pattern) { + if (prev.count.length > 1) { + prev.count.pop(); + } + + prev.count.push(obj.count[0]); + prev.string = prev.pattern + toQuantifier(prev.count); + start = max + 1; + continue; + } + + if (tok.isPadded) { + zeros = padZeros(max, tok, options); + } + + obj.string = zeros + obj.pattern + toQuantifier(obj.count); + tokens.push(obj); + start = max + 1; + prev = obj; + } + + return tokens; +} + +function filterPatterns(arr, comparison, prefix, intersection, options) { + let result = []; + + for (let ele of arr) { + let { string } = ele; + + // only push if _both_ are negative... + if (!intersection && !contains(comparison, 'string', string)) { + result.push(prefix + string); + } + + // or _both_ are positive + if (intersection && contains(comparison, 'string', string)) { + result.push(prefix + string); + } + } + return result; +} + +/** + * Zip strings + */ + +function zip(a, b) { + let arr = []; + for (let i = 0; i < a.length; i++) arr.push([a[i], b[i]]); + return arr; +} + +function compare(a, b) { + return a > b ? 1 : b > a ? -1 : 0; +} + +function contains(arr, key, val) { + return arr.some(ele => ele[key] === val); +} + +function countNines(min, len) { + return Number(String(min).slice(0, -len) + '9'.repeat(len)); +} + +function countZeros(integer, zeros) { + return integer - (integer % Math.pow(10, zeros)); +} + +function toQuantifier(digits) { + let [start = 0, stop = ''] = digits; + if (stop || start > 1) { + return `{${start + (stop ? ',' + stop : '')}}`; + } + return ''; +} + +function toCharacterClass(a, b, options) { + return `[${a}${(b - a === 1) ? '' : '-'}${b}]`; +} + +function hasPadding(str) { + return /^-?(0+)\d/.test(str); +} + +function padZeros(value, tok, options) { + if (!tok.isPadded) { + return value; + } + + let diff = Math.abs(tok.maxLen - String(value).length); + let relax = options.relaxZeros !== false; + + switch (diff) { + case 0: + return ''; + case 1: + return relax ? '0?' : '0'; + case 2: + return relax ? '0{0,2}' : '00'; + default: { + return relax ? `0{0,${diff}}` : `0{${diff}}`; + } + } +} + +/** + * Cache + */ + +toRegexRange.cache = {}; +toRegexRange.clearCache = () => (toRegexRange.cache = {}); + +/** + * Expose `toRegexRange` + */ + +module.exports = toRegexRange; + + +/***/ }), +/* 790 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ + + + +module.exports = function(num) { + if (typeof num === 'number') { + return num - num === 0; + } + if (typeof num === 'string' && num.trim() !== '') { + return Number.isFinite ? Number.isFinite(+num) : isFinite(+num); + } + return false; +}; + + +/***/ }), +/* 791 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const fill = __webpack_require__(788); +const stringify = __webpack_require__(785); +const utils = __webpack_require__(786); + +const append = (queue = '', stash = '', enclose = false) => { + let result = []; + + queue = [].concat(queue); + stash = [].concat(stash); + + if (!stash.length) return queue; + if (!queue.length) { + return enclose ? utils.flatten(stash).map(ele => `{${ele}}`) : stash; + } + + for (let item of queue) { + if (Array.isArray(item)) { + for (let value of item) { + result.push(append(value, stash, enclose)); + } + } else { + for (let ele of stash) { + if (enclose === true && typeof ele === 'string') ele = `{${ele}}`; + result.push(Array.isArray(ele) ? append(item, ele, enclose) : (item + ele)); + } + } + } + return utils.flatten(result); +}; + +const expand = (ast, options = {}) => { + let rangeLimit = options.rangeLimit === void 0 ? 1000 : options.rangeLimit; + + let walk = (node, parent = {}) => { + node.queue = []; + + let p = parent; + let q = parent.queue; + + while (p.type !== 'brace' && p.type !== 'root' && p.parent) { + p = p.parent; + q = p.queue; + } + + if (node.invalid || node.dollar) { + q.push(append(q.pop(), stringify(node, options))); + return; + } + + if (node.type === 'brace' && node.invalid !== true && node.nodes.length === 2) { + q.push(append(q.pop(), ['{}'])); + return; + } + + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + + if (utils.exceedsLimit(...args, options.step, rangeLimit)) { + throw new RangeError('expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.'); + } + + let range = fill(...args, options); + if (range.length === 0) { + range = stringify(node, options); + } + + q.push(append(q.pop(), range)); + node.nodes = []; + return; + } + + let enclose = utils.encloseBrace(node); + let queue = node.queue; + let block = node; + + while (block.type !== 'brace' && block.type !== 'root' && block.parent) { + block = block.parent; + queue = block.queue; + } + + for (let i = 0; i < node.nodes.length; i++) { + let child = node.nodes[i]; + + if (child.type === 'comma' && node.type === 'brace') { + if (i === 1) queue.push(''); + queue.push(''); + continue; + } + + if (child.type === 'close') { + q.push(append(q.pop(), queue, enclose)); + continue; + } + + if (child.value && child.type !== 'open') { + queue.push(append(queue.pop(), child.value)); + continue; + } + + if (child.nodes) { + walk(child, node); + } + } + + return queue; + }; + + return utils.flatten(walk(ast)); +}; + +module.exports = expand; + + +/***/ }), +/* 792 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const stringify = __webpack_require__(785); + +/** + * Constants + */ + +const { + MAX_LENGTH, + CHAR_BACKSLASH, /* \ */ + CHAR_BACKTICK, /* ` */ + CHAR_COMMA, /* , */ + CHAR_DOT, /* . */ + CHAR_LEFT_PARENTHESES, /* ( */ + CHAR_RIGHT_PARENTHESES, /* ) */ + CHAR_LEFT_CURLY_BRACE, /* { */ + CHAR_RIGHT_CURLY_BRACE, /* } */ + CHAR_LEFT_SQUARE_BRACKET, /* [ */ + CHAR_RIGHT_SQUARE_BRACKET, /* ] */ + CHAR_DOUBLE_QUOTE, /* " */ + CHAR_SINGLE_QUOTE, /* ' */ + CHAR_NO_BREAK_SPACE, + CHAR_ZERO_WIDTH_NOBREAK_SPACE +} = __webpack_require__(793); + +/** + * parse + */ + +const parse = (input, options = {}) => { + if (typeof input !== 'string') { + throw new TypeError('Expected a string'); + } + + let opts = options || {}; + let max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; + if (input.length > max) { + throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`); + } + + let ast = { type: 'root', input, nodes: [] }; + let stack = [ast]; + let block = ast; + let prev = ast; + let brackets = 0; + let length = input.length; + let index = 0; + let depth = 0; + let value; + let memo = {}; + + /** + * Helpers + */ + + const advance = () => input[index++]; + const push = node => { + if (node.type === 'text' && prev.type === 'dot') { + prev.type = 'text'; + } + + if (prev && prev.type === 'text' && node.type === 'text') { + prev.value += node.value; + return; + } + + block.nodes.push(node); + node.parent = block; + node.prev = prev; + prev = node; + return node; + }; + + push({ type: 'bos' }); + + while (index < length) { + block = stack[stack.length - 1]; + value = advance(); + + /** + * Invalid chars + */ + + if (value === CHAR_ZERO_WIDTH_NOBREAK_SPACE || value === CHAR_NO_BREAK_SPACE) { + continue; + } + + /** + * Escaped chars + */ + + if (value === CHAR_BACKSLASH) { + push({ type: 'text', value: (options.keepEscaping ? value : '') + advance() }); + continue; + } + + /** + * Right square bracket (literal): ']' + */ + + if (value === CHAR_RIGHT_SQUARE_BRACKET) { + push({ type: 'text', value: '\\' + value }); + continue; + } + + /** + * Left square bracket: '[' + */ + + if (value === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + + let closed = true; + let next; + + while (index < length && (next = advance())) { + value += next; + + if (next === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + continue; + } + + if (next === CHAR_BACKSLASH) { + value += advance(); + continue; + } + + if (next === CHAR_RIGHT_SQUARE_BRACKET) { + brackets--; + + if (brackets === 0) { + break; + } + } + } + + push({ type: 'text', value }); + continue; + } + + /** + * Parentheses + */ + + if (value === CHAR_LEFT_PARENTHESES) { + block = push({ type: 'paren', nodes: [] }); + stack.push(block); + push({ type: 'text', value }); + continue; + } + + if (value === CHAR_RIGHT_PARENTHESES) { + if (block.type !== 'paren') { + push({ type: 'text', value }); + continue; + } + block = stack.pop(); + push({ type: 'text', value }); + block = stack[stack.length - 1]; + continue; + } + + /** + * Quotes: '|"|` + */ + + if (value === CHAR_DOUBLE_QUOTE || value === CHAR_SINGLE_QUOTE || value === CHAR_BACKTICK) { + let open = value; + let next; + + if (options.keepQuotes !== true) { + value = ''; + } + + while (index < length && (next = advance())) { + if (next === CHAR_BACKSLASH) { + value += next + advance(); + continue; + } + + if (next === open) { + if (options.keepQuotes === true) value += next; + break; + } + + value += next; + } + + push({ type: 'text', value }); + continue; + } + + /** + * Left curly brace: '{' + */ + + if (value === CHAR_LEFT_CURLY_BRACE) { + depth++; + + let dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true; + let brace = { + type: 'brace', + open: true, + close: false, + dollar, + depth, + commas: 0, + ranges: 0, + nodes: [] + }; + + block = push(brace); + stack.push(block); + push({ type: 'open', value }); + continue; + } + + /** + * Right curly brace: '}' + */ + + if (value === CHAR_RIGHT_CURLY_BRACE) { + if (block.type !== 'brace') { + push({ type: 'text', value }); + continue; + } + + let type = 'close'; + block = stack.pop(); + block.close = true; + + push({ type, value }); + depth--; + + block = stack[stack.length - 1]; + continue; + } + + /** + * Comma: ',' + */ + + if (value === CHAR_COMMA && depth > 0) { + if (block.ranges > 0) { + block.ranges = 0; + let open = block.nodes.shift(); + block.nodes = [open, { type: 'text', value: stringify(block) }]; + } + + push({ type: 'comma', value }); + block.commas++; + continue; + } + + /** + * Dot: '.' + */ + + if (value === CHAR_DOT && depth > 0 && block.commas === 0) { + let siblings = block.nodes; + + if (depth === 0 || siblings.length === 0) { + push({ type: 'text', value }); + continue; + } + + if (prev.type === 'dot') { + block.range = []; + prev.value += value; + prev.type = 'range'; + + if (block.nodes.length !== 3 && block.nodes.length !== 5) { + block.invalid = true; + block.ranges = 0; + prev.type = 'text'; + continue; + } + + block.ranges++; + block.args = []; + continue; + } + + if (prev.type === 'range') { + siblings.pop(); + + let before = siblings[siblings.length - 1]; + before.value += prev.value + value; + prev = before; + block.ranges--; + continue; + } + + push({ type: 'dot', value }); + continue; + } + + /** + * Text + */ + + push({ type: 'text', value }); + } + + // Mark imbalanced braces and brackets as invalid + do { + block = stack.pop(); + + if (block.type !== 'root') { + block.nodes.forEach(node => { + if (!node.nodes) { + if (node.type === 'open') node.isOpen = true; + if (node.type === 'close') node.isClose = true; + if (!node.nodes) node.type = 'text'; + node.invalid = true; + } + }); + + // get the location of the block on parent.nodes (block's siblings) + let parent = stack[stack.length - 1]; + let index = parent.nodes.indexOf(block); + // replace the (invalid) block with it's nodes + parent.nodes.splice(index, 1, ...block.nodes); + } + } while (stack.length > 0); + + push({ type: 'eos' }); + return ast; +}; + +module.exports = parse; + + +/***/ }), +/* 793 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = { + MAX_LENGTH: 1024 * 64, + + // Digits + CHAR_0: '0', /* 0 */ + CHAR_9: '9', /* 9 */ + + // Alphabet chars. + CHAR_UPPERCASE_A: 'A', /* A */ + CHAR_LOWERCASE_A: 'a', /* a */ + CHAR_UPPERCASE_Z: 'Z', /* Z */ + CHAR_LOWERCASE_Z: 'z', /* z */ + + CHAR_LEFT_PARENTHESES: '(', /* ( */ + CHAR_RIGHT_PARENTHESES: ')', /* ) */ + + CHAR_ASTERISK: '*', /* * */ + + // Non-alphabetic chars. + CHAR_AMPERSAND: '&', /* & */ + CHAR_AT: '@', /* @ */ + CHAR_BACKSLASH: '\\', /* \ */ + CHAR_BACKTICK: '`', /* ` */ + CHAR_CARRIAGE_RETURN: '\r', /* \r */ + CHAR_CIRCUMFLEX_ACCENT: '^', /* ^ */ + CHAR_COLON: ':', /* : */ + CHAR_COMMA: ',', /* , */ + CHAR_DOLLAR: '$', /* . */ + CHAR_DOT: '.', /* . */ + CHAR_DOUBLE_QUOTE: '"', /* " */ + CHAR_EQUAL: '=', /* = */ + CHAR_EXCLAMATION_MARK: '!', /* ! */ + CHAR_FORM_FEED: '\f', /* \f */ + CHAR_FORWARD_SLASH: '/', /* / */ + CHAR_HASH: '#', /* # */ + CHAR_HYPHEN_MINUS: '-', /* - */ + CHAR_LEFT_ANGLE_BRACKET: '<', /* < */ + CHAR_LEFT_CURLY_BRACE: '{', /* { */ + CHAR_LEFT_SQUARE_BRACKET: '[', /* [ */ + CHAR_LINE_FEED: '\n', /* \n */ + CHAR_NO_BREAK_SPACE: '\u00A0', /* \u00A0 */ + CHAR_PERCENT: '%', /* % */ + CHAR_PLUS: '+', /* + */ + CHAR_QUESTION_MARK: '?', /* ? */ + CHAR_RIGHT_ANGLE_BRACKET: '>', /* > */ + CHAR_RIGHT_CURLY_BRACE: '}', /* } */ + CHAR_RIGHT_SQUARE_BRACKET: ']', /* ] */ + CHAR_SEMICOLON: ';', /* ; */ + CHAR_SINGLE_QUOTE: '\'', /* ' */ + CHAR_SPACE: ' ', /* */ + CHAR_TAB: '\t', /* \t */ + CHAR_UNDERSCORE: '_', /* _ */ + CHAR_VERTICAL_LINE: '|', /* | */ + CHAR_ZERO_WIDTH_NOBREAK_SPACE: '\uFEFF' /* \uFEFF */ +}; + + +/***/ }), +/* 794 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.merge = void 0; +const merge2 = __webpack_require__(146); +function merge(streams) { + const mergedStream = merge2(streams); + streams.forEach((stream) => { + stream.once('error', (error) => mergedStream.emit('error', error)); + }); + mergedStream.once('close', () => propagateCloseEventToSources(streams)); + mergedStream.once('end', () => propagateCloseEventToSources(streams)); + return mergedStream; +} +exports.merge = merge; +function propagateCloseEventToSources(streams) { + streams.forEach((stream) => stream.emit('close')); +} + + +/***/ }), +/* 795 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmpty = exports.isString = void 0; +function isString(input) { + return typeof input === 'string'; +} +exports.isString = isString; +function isEmpty(input) { + return input === ''; +} +exports.isEmpty = isEmpty; + + +/***/ }), +/* 796 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(797); +const provider_1 = __webpack_require__(799); +class ProviderAsync extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new stream_1.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const entries = []; + return new Promise((resolve, reject) => { + const stream = this.api(root, task, options); + stream.once('error', reject); + stream.on('data', (entry) => entries.push(options.transform(entry))); + stream.once('end', () => resolve(entries)); + }); + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderAsync; + + +/***/ }), +/* 797 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(138); +const fsStat = __webpack_require__(195); +const fsWalk = __webpack_require__(200); +const reader_1 = __webpack_require__(798); +class ReaderStream extends reader_1.default { + constructor() { + super(...arguments); + this._walkStream = fsWalk.walkStream; + this._stat = fsStat.stat; + } + dynamic(root, options) { + return this._walkStream(root, options); + } + static(patterns, options) { + const filepaths = patterns.map(this._getFullEntryPath, this); + const stream = new stream_1.PassThrough({ objectMode: true }); + stream._write = (index, _enc, done) => { + return this._getEntry(filepaths[index], patterns[index], options) + .then((entry) => { + if (entry !== null && options.entryFilter(entry)) { + stream.push(entry); + } + if (index === filepaths.length - 1) { + stream.end(); + } + done(); + }) + .catch(done); + }; + for (let i = 0; i < filepaths.length; i++) { + stream.write(i); + } + return stream; + } + _getEntry(filepath, pattern, options) { + return this._getStat(filepath) + .then((stats) => this._makeEntry(stats, pattern)) + .catch((error) => { + if (options.errorFilter(error)) { + return null; + } + throw error; + }); + } + _getStat(filepath) { + return new Promise((resolve, reject) => { + this._stat(filepath, this._fsStatSettings, (error, stats) => { + return error === null ? resolve(stats) : reject(error); + }); + }); + } +} +exports.default = ReaderStream; + + +/***/ }), +/* 798 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __webpack_require__(4); +const fsStat = __webpack_require__(195); +const utils = __webpack_require__(777); +class Reader { + constructor(_settings) { + this._settings = _settings; + this._fsStatSettings = new fsStat.Settings({ + followSymbolicLink: this._settings.followSymbolicLinks, + fs: this._settings.fs, + throwErrorOnBrokenSymbolicLink: this._settings.followSymbolicLinks + }); + } + _getFullEntryPath(filepath) { + return path.resolve(this._settings.cwd, filepath); + } + _makeEntry(stats, pattern) { + const entry = { + name: pattern, + path: pattern, + dirent: utils.fs.createDirentFromStats(pattern, stats) + }; + if (this._settings.stats) { + entry.stats = stats; + } + return entry; + } + _isFatalError(error) { + return !utils.errno.isEnoentCodeError(error) && !this._settings.suppressErrors; + } +} +exports.default = Reader; + + +/***/ }), +/* 799 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __webpack_require__(4); +const deep_1 = __webpack_require__(800); +const entry_1 = __webpack_require__(803); +const error_1 = __webpack_require__(804); +const entry_2 = __webpack_require__(805); +class Provider { + constructor(_settings) { + this._settings = _settings; + this.errorFilter = new error_1.default(this._settings); + this.entryFilter = new entry_1.default(this._settings, this._getMicromatchOptions()); + this.deepFilter = new deep_1.default(this._settings, this._getMicromatchOptions()); + this.entryTransformer = new entry_2.default(this._settings); + } + _getRootDirectory(task) { + return path.resolve(this._settings.cwd, task.base); + } + _getReaderOptions(task) { + const basePath = task.base === '.' ? '' : task.base; + return { + basePath, + pathSegmentSeparator: '/', + concurrency: this._settings.concurrency, + deepFilter: this.deepFilter.getFilter(basePath, task.positive, task.negative), + entryFilter: this.entryFilter.getFilter(task.positive, task.negative), + errorFilter: this.errorFilter.getFilter(), + followSymbolicLinks: this._settings.followSymbolicLinks, + fs: this._settings.fs, + stats: this._settings.stats, + throwErrorOnBrokenSymbolicLink: this._settings.throwErrorOnBrokenSymbolicLink, + transform: this.entryTransformer.getTransformer() + }; + } + _getMicromatchOptions() { + return { + dot: this._settings.dot, + matchBase: this._settings.baseNameMatch, + nobrace: !this._settings.braceExpansion, + nocase: !this._settings.caseSensitiveMatch, + noext: !this._settings.extglob, + noglobstar: !this._settings.globstar, + posix: true, + strictSlashes: false + }; + } +} +exports.default = Provider; + + +/***/ }), +/* 800 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +const partial_1 = __webpack_require__(801); +class DeepFilter { + constructor(_settings, _micromatchOptions) { + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + } + getFilter(basePath, positive, negative) { + const matcher = this._getMatcher(positive); + const negativeRe = this._getNegativePatternsRe(negative); + return (entry) => this._filter(basePath, entry, matcher, negativeRe); + } + _getMatcher(patterns) { + return new partial_1.default(patterns, this._settings, this._micromatchOptions); + } + _getNegativePatternsRe(patterns) { + const affectDepthOfReadingPatterns = patterns.filter(utils.pattern.isAffectDepthOfReadingPattern); + return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions); + } + _filter(basePath, entry, matcher, negativeRe) { + if (this._isSkippedByDeep(basePath, entry.path)) { + return false; + } + if (this._isSkippedSymbolicLink(entry)) { + return false; + } + const filepath = utils.path.removeLeadingDotSegment(entry.path); + if (this._isSkippedByPositivePatterns(filepath, matcher)) { + return false; + } + return this._isSkippedByNegativePatterns(filepath, negativeRe); + } + _isSkippedByDeep(basePath, entryPath) { + /** + * Avoid unnecessary depth calculations when it doesn't matter. + */ + if (this._settings.deep === Infinity) { + return false; + } + return this._getEntryLevel(basePath, entryPath) >= this._settings.deep; + } + _getEntryLevel(basePath, entryPath) { + const entryPathDepth = entryPath.split('/').length; + if (basePath === '') { + return entryPathDepth; + } + const basePathDepth = basePath.split('/').length; + return entryPathDepth - basePathDepth; + } + _isSkippedSymbolicLink(entry) { + return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); + } + _isSkippedByPositivePatterns(entryPath, matcher) { + return !this._settings.baseNameMatch && !matcher.match(entryPath); + } + _isSkippedByNegativePatterns(entryPath, patternsRe) { + return !utils.pattern.matchAny(entryPath, patternsRe); + } +} +exports.default = DeepFilter; + + +/***/ }), +/* 801 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const matcher_1 = __webpack_require__(802); +class PartialMatcher extends matcher_1.default { + match(filepath) { + const parts = filepath.split('/'); + const levels = parts.length; + const patterns = this._storage.filter((info) => !info.complete || info.segments.length > levels); + for (const pattern of patterns) { + const section = pattern.sections[0]; + /** + * In this case, the pattern has a globstar and we must read all directories unconditionally, + * but only if the level has reached the end of the first group. + * + * fixtures/{a,b}/** + * ^ true/false ^ always true + */ + if (!pattern.complete && levels > section.length) { + return true; + } + const match = parts.every((part, index) => { + const segment = pattern.segments[index]; + if (segment.dynamic && segment.patternRe.test(part)) { + return true; + } + if (!segment.dynamic && segment.pattern === part) { + return true; + } + return false; + }); + if (match) { + return true; + } + } + return false; + } +} +exports.default = PartialMatcher; + + +/***/ }), +/* 802 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class Matcher { + constructor(_patterns, _settings, _micromatchOptions) { + this._patterns = _patterns; + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + this._storage = []; + this._fillStorage(); + } + _fillStorage() { + /** + * The original pattern may include `{,*,**,a/*}`, which will lead to problems with matching (unresolved level). + * So, before expand patterns with brace expansion into separated patterns. + */ + const patterns = utils.pattern.expandPatternsWithBraceExpansion(this._patterns); + for (const pattern of patterns) { + const segments = this._getPatternSegments(pattern); + const sections = this._splitSegmentsIntoSections(segments); + this._storage.push({ + complete: sections.length <= 1, + pattern, + segments, + sections + }); + } + } + _getPatternSegments(pattern) { + const parts = utils.pattern.getPatternParts(pattern, this._micromatchOptions); + return parts.map((part) => { + const dynamic = utils.pattern.isDynamicPattern(part, this._settings); + if (!dynamic) { + return { + dynamic: false, + pattern: part + }; + } + return { + dynamic: true, + pattern: part, + patternRe: utils.pattern.makeRe(part, this._micromatchOptions) + }; + }); + } + _splitSegmentsIntoSections(segments) { + return utils.array.splitWhen(segments, (segment) => segment.dynamic && utils.pattern.hasGlobStar(segment.pattern)); + } +} +exports.default = Matcher; + + +/***/ }), +/* 803 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class EntryFilter { + constructor(_settings, _micromatchOptions) { + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + this.index = new Map(); + } + getFilter(positive, negative) { + const positiveRe = utils.pattern.convertPatternsToRe(positive, this._micromatchOptions); + const negativeRe = utils.pattern.convertPatternsToRe(negative, this._micromatchOptions); + return (entry) => this._filter(entry, positiveRe, negativeRe); + } + _filter(entry, positiveRe, negativeRe) { + if (this._settings.unique && this._isDuplicateEntry(entry)) { + return false; + } + if (this._onlyFileFilter(entry) || this._onlyDirectoryFilter(entry)) { + return false; + } + if (this._isSkippedByAbsoluteNegativePatterns(entry.path, negativeRe)) { + return false; + } + const filepath = this._settings.baseNameMatch ? entry.name : entry.path; + const isMatched = this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + if (this._settings.unique && isMatched) { + this._createIndexRecord(entry); + } + return isMatched; + } + _isDuplicateEntry(entry) { + return this.index.has(entry.path); + } + _createIndexRecord(entry) { + this.index.set(entry.path, undefined); + } + _onlyFileFilter(entry) { + return this._settings.onlyFiles && !entry.dirent.isFile(); + } + _onlyDirectoryFilter(entry) { + return this._settings.onlyDirectories && !entry.dirent.isDirectory(); + } + _isSkippedByAbsoluteNegativePatterns(entryPath, patternsRe) { + if (!this._settings.absolute) { + return false; + } + const fullpath = utils.path.makeAbsolute(this._settings.cwd, entryPath); + return utils.pattern.matchAny(fullpath, patternsRe); + } + _isMatchToPatterns(entryPath, patternsRe) { + const filepath = utils.path.removeLeadingDotSegment(entryPath); + return utils.pattern.matchAny(filepath, patternsRe); + } +} +exports.default = EntryFilter; - for (let i = 0; i < arguments.length; i++) { - results[i] = arguments[i]; - } - resolve(results); - } else { - resolve(result); - } - }); - } +/***/ }), +/* 804 */ +/***/ (function(module, exports, __webpack_require__) { - fn.apply(this, args); - }); -}; +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class ErrorFilter { + constructor(_settings) { + this._settings = _settings; + } + getFilter() { + return (error) => this._isNonFatalError(error); + } + _isNonFatalError(error) { + return utils.errno.isEnoentCodeError(error) || this._settings.suppressErrors; + } +} +exports.default = ErrorFilter; -module.exports = (obj, opts) => { - opts = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, opts); - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return opts.include ? opts.include.some(match) : !opts.exclude.some(match); - }; +/***/ }), +/* 805 */ +/***/ (function(module, exports, __webpack_require__) { - let ret; - if (typeof obj === 'function') { - ret = function () { - if (opts.excludeMain) { - return obj.apply(this, arguments); - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class EntryTransformer { + constructor(_settings) { + this._settings = _settings; + } + getTransformer() { + return (entry) => this._transform(entry); + } + _transform(entry) { + let filepath = entry.path; + if (this._settings.absolute) { + filepath = utils.path.makeAbsolute(this._settings.cwd, filepath); + filepath = utils.path.unixify(filepath); + } + if (this._settings.markDirectories && entry.dirent.isDirectory()) { + filepath += '/'; + } + if (!this._settings.objectMode) { + return filepath; + } + return Object.assign(Object.assign({}, entry), { path: filepath }); + } +} +exports.default = EntryTransformer; - return processFn(obj, opts).apply(this, arguments); - }; - } else { - ret = Object.create(Object.getPrototypeOf(obj)); - } - for (const key in obj) { // eslint-disable-line guard-for-in - const x = obj[key]; - ret[key] = typeof x === 'function' && filter(key) ? processFn(x, opts) : x; - } +/***/ }), +/* 806 */ +/***/ (function(module, exports, __webpack_require__) { - return ret; -}; +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(138); +const stream_2 = __webpack_require__(797); +const provider_1 = __webpack_require__(799); +class ProviderStream extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new stream_2.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const source = this.api(root, task, options); + const destination = new stream_1.Readable({ objectMode: true, read: () => { } }); + source + .once('error', (error) => destination.emit('error', error)) + .on('data', (entry) => destination.emit('data', options.transform(entry))) + .once('end', () => destination.emit('end')); + destination + .once('close', () => source.destroy()); + return destination; + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderStream; /***/ }), -/* 780 */ +/* 807 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const sync_1 = __webpack_require__(808); +const provider_1 = __webpack_require__(799); +class ProviderSync extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new sync_1.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const entries = this.api(root, task, options); + return entries.map(options.transform); + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderSync; + + +/***/ }), +/* 808 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const fsStat = __webpack_require__(195); +const fsWalk = __webpack_require__(200); +const reader_1 = __webpack_require__(798); +class ReaderSync extends reader_1.default { + constructor() { + super(...arguments); + this._walkSync = fsWalk.walkSync; + this._statSync = fsStat.statSync; + } + dynamic(root, options) { + return this._walkSync(root, options); + } + static(patterns, options) { + const entries = []; + for (const pattern of patterns) { + const filepath = this._getFullEntryPath(pattern); + const entry = this._getEntry(filepath, pattern, options); + if (entry === null || !options.entryFilter(entry)) { + continue; + } + entries.push(entry); + } + return entries; + } + _getEntry(filepath, pattern, options) { + try { + const stats = this._getStat(filepath); + return this._makeEntry(stats, pattern); + } + catch (error) { + if (options.errorFilter(error)) { + return null; + } + throw error; + } + } + _getStat(filepath) { + return this._statSync(filepath, this._fsStatSettings); + } +} +exports.default = ReaderSync; + + +/***/ }), +/* 809 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0; +const fs = __webpack_require__(134); +const os = __webpack_require__(121); +/** + * The `os.cpus` method can return zero. We expect the number of cores to be greater than zero. + * https://github.com/nodejs/node/blob/7faeddf23a98c53896f8b574a6e66589e8fb1eb8/lib/os.js#L106-L107 + */ +const CPU_COUNT = Math.max(os.cpus().length, 1); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = { + lstat: fs.lstat, + lstatSync: fs.lstatSync, + stat: fs.stat, + statSync: fs.statSync, + readdir: fs.readdir, + readdirSync: fs.readdirSync +}; +class Settings { + constructor(_options = {}) { + this._options = _options; + this.absolute = this._getValue(this._options.absolute, false); + this.baseNameMatch = this._getValue(this._options.baseNameMatch, false); + this.braceExpansion = this._getValue(this._options.braceExpansion, true); + this.caseSensitiveMatch = this._getValue(this._options.caseSensitiveMatch, true); + this.concurrency = this._getValue(this._options.concurrency, CPU_COUNT); + this.cwd = this._getValue(this._options.cwd, process.cwd()); + this.deep = this._getValue(this._options.deep, Infinity); + this.dot = this._getValue(this._options.dot, false); + this.extglob = this._getValue(this._options.extglob, true); + this.followSymbolicLinks = this._getValue(this._options.followSymbolicLinks, true); + this.fs = this._getFileSystemMethods(this._options.fs); + this.globstar = this._getValue(this._options.globstar, true); + this.ignore = this._getValue(this._options.ignore, []); + this.markDirectories = this._getValue(this._options.markDirectories, false); + this.objectMode = this._getValue(this._options.objectMode, false); + this.onlyDirectories = this._getValue(this._options.onlyDirectories, false); + this.onlyFiles = this._getValue(this._options.onlyFiles, true); + this.stats = this._getValue(this._options.stats, false); + this.suppressErrors = this._getValue(this._options.suppressErrors, false); + this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, false); + this.unique = this._getValue(this._options.unique, true); + if (this.onlyDirectories) { + this.onlyFiles = false; + } + if (this.stats) { + this.objectMode = true; + } + } + _getValue(option, value) { + return option === undefined ? value : option; + } + _getFileSystemMethods(methods = {}) { + return Object.assign(Object.assign({}, exports.DEFAULT_FILE_SYSTEM_ADAPTER), methods); + } +} +exports.default = Settings; + + +/***/ }), +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(572); -const gitIgnore = __webpack_require__(781); -const pify = __webpack_require__(779); -const slash = __webpack_require__(782); +const fastGlob = __webpack_require__(775); +const gitIgnore = __webpack_require__(235); +const slash = __webpack_require__(236); const DEFAULT_IGNORE = [ '**/node_modules/**', - '**/bower_components/**', '**/flow-typed/**', '**/coverage/**', '**/.git' ]; -const readFileP = pify(fs.readFile); +const readFileP = promisify(fs.readFile); const mapGitIgnorePatternTo = base => ignore => { if (ignore.startsWith('!')) { - return '!' + path.posix.join(base, ignore.substr(1)); + return '!' + path.posix.join(base, ignore.slice(1)); } return path.posix.join(base, ignore); }; -const parseGitIgnore = (content, opts) => { - const base = slash(path.relative(opts.cwd, path.dirname(opts.fileName))); +const parseGitIgnore = (content, options) => { + const base = slash(path.relative(options.cwd, path.dirname(options.fileName))); return content .split(/\r?\n/) .filter(Boolean) - .filter(l => l.charAt(0) !== '#') + .filter(line => !line.startsWith('#')) .map(mapGitIgnorePatternTo(base)); }; const reduceIgnore = files => { - return files.reduce((ignores, file) => { + const ignores = gitIgnore(); + for (const file of files) { ignores.add(parseGitIgnore(file.content, { cwd: file.cwd, fileName: file.filePath })); - return ignores; - }, gitIgnore()); + } + + return ignores; +}; + +const ensureAbsolutePathForCwd = (cwd, p) => { + cwd = slash(cwd); + if (path.isAbsolute(p)) { + if (slash(p).startsWith(cwd)) { + return p; + } + + throw new Error(`Path ${p} is not in cwd ${cwd}`); + } + + return path.join(cwd, p); }; const getIsIgnoredPredecate = (ignores, cwd) => { - return p => ignores.ignores(slash(path.relative(cwd, p))); + return p => ignores.ignores(slash(path.relative(cwd, ensureAbsolutePathForCwd(cwd, p.path || p)))); }; -const getFile = (file, cwd) => { +const getFile = async (file, cwd) => { const filePath = path.join(cwd, file); - return readFileP(filePath, 'utf8') - .then(content => ({ - content, - cwd, - filePath - })); + const content = await readFileP(filePath, 'utf8'); + + return { + cwd, + filePath, + content + }; }; const getFileSync = (file, cwd) => { @@ -90686,490 +93669,103 @@ const getFileSync = (file, cwd) => { const content = fs.readFileSync(filePath, 'utf8'); return { - content, cwd, - filePath + filePath, + content }; }; -const normalizeOpts = opts => { - opts = opts || {}; - const ignore = opts.ignore || []; - const cwd = opts.cwd || process.cwd(); +const normalizeOptions = ({ + ignore = [], + cwd = slash(process.cwd()) +} = {}) => { return {ignore, cwd}; }; -module.exports = o => { - const opts = normalizeOpts(o); - - return fastGlob('**/.gitignore', {ignore: DEFAULT_IGNORE.concat(opts.ignore), cwd: opts.cwd}) - .then(paths => Promise.all(paths.map(file => getFile(file, opts.cwd)))) - .then(files => reduceIgnore(files)) - .then(ignores => getIsIgnoredPredecate(ignores, opts.cwd)); -}; +module.exports = async options => { + options = normalizeOptions(options); -module.exports.sync = o => { - const opts = normalizeOpts(o); + const paths = await fastGlob('**/.gitignore', { + ignore: DEFAULT_IGNORE.concat(options.ignore), + cwd: options.cwd + }); - const paths = fastGlob.sync('**/.gitignore', {ignore: DEFAULT_IGNORE.concat(opts.ignore), cwd: opts.cwd}); - const files = paths.map(file => getFileSync(file, opts.cwd)); + const files = await Promise.all(paths.map(file => getFile(file, options.cwd))); const ignores = reduceIgnore(files); - return getIsIgnoredPredecate(ignores, opts.cwd); -}; - - -/***/ }), -/* 781 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -module.exports = function () { - return new IgnoreBase(); + return getIsIgnoredPredecate(ignores, options.cwd); }; -// A simple implementation of make-array -function make_array(subject) { - return Array.isArray(subject) ? subject : [subject]; -} - -var REGEX_BLANK_LINE = /^\s+$/; -var REGEX_LEADING_EXCAPED_EXCLAMATION = /^\\\!/; -var REGEX_LEADING_EXCAPED_HASH = /^\\#/; -var SLASH = '/'; -var KEY_IGNORE = typeof Symbol !== 'undefined' ? Symbol.for('node-ignore') -/* istanbul ignore next */ -: 'node-ignore'; - -var IgnoreBase = function () { - function IgnoreBase() { - _classCallCheck(this, IgnoreBase); - - this._rules = []; - this[KEY_IGNORE] = true; - this._initCache(); - } - - _createClass(IgnoreBase, [{ - key: '_initCache', - value: function _initCache() { - this._cache = {}; - } - - // @param {Array.|string|Ignore} pattern - - }, { - key: 'add', - value: function add(pattern) { - this._added = false; - - if (typeof pattern === 'string') { - pattern = pattern.split(/\r?\n/g); - } - - make_array(pattern).forEach(this._addPattern, this); - - // Some rules have just added to the ignore, - // making the behavior changed. - if (this._added) { - this._initCache(); - } - - return this; - } - - // legacy - - }, { - key: 'addPattern', - value: function addPattern(pattern) { - return this.add(pattern); - } - }, { - key: '_addPattern', - value: function _addPattern(pattern) { - // #32 - if (pattern && pattern[KEY_IGNORE]) { - this._rules = this._rules.concat(pattern._rules); - this._added = true; - return; - } - - if (this._checkPattern(pattern)) { - var rule = this._createRule(pattern); - this._added = true; - this._rules.push(rule); - } - } - }, { - key: '_checkPattern', - value: function _checkPattern(pattern) { - // > A blank line matches no files, so it can serve as a separator for readability. - return pattern && typeof pattern === 'string' && !REGEX_BLANK_LINE.test(pattern) - - // > A line starting with # serves as a comment. - && pattern.indexOf('#') !== 0; - } - }, { - key: 'filter', - value: function filter(paths) { - var _this = this; - - return make_array(paths).filter(function (path) { - return _this._filter(path); - }); - } - }, { - key: 'createFilter', - value: function createFilter() { - var _this2 = this; - - return function (path) { - return _this2._filter(path); - }; - } - }, { - key: 'ignores', - value: function ignores(path) { - return !this._filter(path); - } - }, { - key: '_createRule', - value: function _createRule(pattern) { - var origin = pattern; - var negative = false; - - // > An optional prefix "!" which negates the pattern; - if (pattern.indexOf('!') === 0) { - negative = true; - pattern = pattern.substr(1); - } - - pattern = pattern - // > Put a backslash ("\") in front of the first "!" for patterns that begin with a literal "!", for example, `"\!important!.txt"`. - .replace(REGEX_LEADING_EXCAPED_EXCLAMATION, '!') - // > Put a backslash ("\") in front of the first hash for patterns that begin with a hash. - .replace(REGEX_LEADING_EXCAPED_HASH, '#'); - - var regex = make_regex(pattern, negative); - - return { - origin: origin, - pattern: pattern, - negative: negative, - regex: regex - }; - } - - // @returns `Boolean` true if the `path` is NOT ignored - - }, { - key: '_filter', - value: function _filter(path, slices) { - if (!path) { - return false; - } - - if (path in this._cache) { - return this._cache[path]; - } - - if (!slices) { - // path/to/a.js - // ['path', 'to', 'a.js'] - slices = path.split(SLASH); - } - - slices.pop(); - - return this._cache[path] = slices.length - // > It is not possible to re-include a file if a parent directory of that file is excluded. - // If the path contains a parent directory, check the parent first - ? this._filter(slices.join(SLASH) + SLASH, slices) && this._test(path) - - // Or only test the path - : this._test(path); - } - - // @returns {Boolean} true if a file is NOT ignored - - }, { - key: '_test', - value: function _test(path) { - // Explicitly define variable type by setting matched to `0` - var matched = 0; - - this._rules.forEach(function (rule) { - // if matched = true, then we only test negative rules - // if matched = false, then we test non-negative rules - if (!(matched ^ rule.negative)) { - matched = rule.negative ^ rule.regex.test(path); - } - }); - - return !matched; - } - }]); - - return IgnoreBase; -}(); - -// > If the pattern ends with a slash, -// > it is removed for the purpose of the following description, -// > but it would only find a match with a directory. -// > In other words, foo/ will match a directory foo and paths underneath it, -// > but will not match a regular file or a symbolic link foo -// > (this is consistent with the way how pathspec works in general in Git). -// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`' -// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call -// you could use option `mark: true` with `glob` - -// '`foo/`' should not continue with the '`..`' - - -var DEFAULT_REPLACER_PREFIX = [ - -// > Trailing spaces are ignored unless they are quoted with backslash ("\") -[ -// (a\ ) -> (a ) -// (a ) -> (a) -// (a \ ) -> (a ) -/\\?\s+$/, function (match) { - return match.indexOf('\\') === 0 ? ' ' : ''; -}], - -// replace (\ ) with ' ' -[/\\\s/g, function () { - return ' '; -}], - -// Escape metacharacters -// which is written down by users but means special for regular expressions. - -// > There are 12 characters with special meanings: -// > - the backslash \, -// > - the caret ^, -// > - the dollar sign $, -// > - the period or dot ., -// > - the vertical bar or pipe symbol |, -// > - the question mark ?, -// > - the asterisk or star *, -// > - the plus sign +, -// > - the opening parenthesis (, -// > - the closing parenthesis ), -// > - and the opening square bracket [, -// > - the opening curly brace {, -// > These special characters are often called "metacharacters". -[/[\\\^$.|?*+()\[{]/g, function (match) { - return '\\' + match; -}], - -// leading slash -[ - -// > A leading slash matches the beginning of the pathname. -// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". -// A leading slash matches the beginning of the pathname -/^\//, function () { - return '^'; -}], - -// replace special metacharacter slash after the leading slash -[/\//g, function () { - return '\\/'; -}], [ -// > A leading "**" followed by a slash means match in all directories. -// > For example, "**/foo" matches file or directory "foo" anywhere, -// > the same as pattern "foo". -// > "**/foo/bar" matches file or directory "bar" anywhere that is directly under directory "foo". -// Notice that the '*'s have been replaced as '\\*' -/^\^*\\\*\\\*\\\//, - -// '**/foo' <-> 'foo' -function () { - return '^(?:.*\\/)?'; -}]]; - -var DEFAULT_REPLACER_SUFFIX = [ -// starting -[ -// there will be no leading '/' (which has been replaced by section "leading slash") -// If starts with '**', adding a '^' to the regular expression also works -/^(?=[^\^])/, function () { - return !/\/(?!$)/.test(this) - // > If the pattern does not contain a slash /, Git treats it as a shell glob pattern - // Actually, if there is only a trailing slash, git also treats it as a shell glob pattern - ? '(?:^|\\/)' - - // > Otherwise, Git treats the pattern as a shell glob suitable for consumption by fnmatch(3) - : '^'; -}], - -// two globstars -[ -// Use lookahead assertions so that we could match more than one `'/**'` -/\\\/\\\*\\\*(?=\\\/|$)/g, - -// Zero, one or several directories -// should not use '*', or it will be replaced by the next replacer - -// Check if it is not the last `'/**'` -function (match, index, str) { - return index + 6 < str.length - - // case: /**/ - // > A slash followed by two consecutive asterisks then a slash matches zero or more directories. - // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on. - // '/**/' - ? '(?:\\/[^\\/]+)*' +module.exports.sync = options => { + options = normalizeOptions(options); - // case: /** - // > A trailing `"/**"` matches everything inside. + const paths = fastGlob.sync('**/.gitignore', { + ignore: DEFAULT_IGNORE.concat(options.ignore), + cwd: options.cwd + }); - // #21: everything inside but it should not include the current folder - : '\\/.+'; -}], + const files = paths.map(file => getFileSync(file, options.cwd)); + const ignores = reduceIgnore(files); -// intermediate wildcards -[ -// Never replace escaped '*' -// ignore rule '\*' will match the path '*' - -// 'abc.*/' -> go -// 'abc.*' -> skip this rule -/(^|[^\\]+)\\\*(?=.+)/g, - -// '*.js' matches '.js' -// '*.js' doesn't match 'abc' -function (match, p1) { - return p1 + '[^\\/]*'; -}], - -// trailing wildcard -[/(\^|\\\/)?\\\*$/, function (match, p1) { - return (p1 - // '\^': - // '/*' does not match '' - // '/*' does not match everything - - // '\\\/': - // 'abc/*' does not match 'abc/' - ? p1 + '[^/]+' - - // 'a*' matches 'a' - // 'a*' matches 'aa' - : '[^/]*') + '(?=$|\\/$)'; -}], [ -// unescape -/\\\\\\/g, function () { - return '\\'; -}]]; - -var POSITIVE_REPLACERS = [].concat(DEFAULT_REPLACER_PREFIX, [ - -// 'f' -// matches -// - /f(end) -// - /f/ -// - (start)f(end) -// - (start)f/ -// doesn't match -// - oof -// - foo -// pseudo: -// -> (^|/)f(/|$) - -// ending -[ -// 'js' will not match 'js.' -// 'ab' will not match 'abc' -/(?:[^*\/])$/, - -// 'js*' will not match 'a.js' -// 'js/' will not match 'a.js' -// 'js' will match 'a.js' and 'a.js/' -function (match) { - return match + '(?=$|\\/)'; -}]], DEFAULT_REPLACER_SUFFIX); - -var NEGATIVE_REPLACERS = [].concat(DEFAULT_REPLACER_PREFIX, [ - -// #24, #38 -// The MISSING rule of [gitignore docs](https://git-scm.com/docs/gitignore) -// A negative pattern without a trailing wildcard should not -// re-include the things inside that directory. - -// eg: -// ['node_modules/*', '!node_modules'] -// should ignore `node_modules/a.js` -[/(?:[^*])$/, function (match) { - return match + '(?=$|\\/$)'; -}]], DEFAULT_REPLACER_SUFFIX); + return getIsIgnoredPredecate(ignores, options.cwd); +}; -// A simple cache, because an ignore rule only has only one certain meaning -var cache = {}; -// @param {pattern} -function make_regex(pattern, negative) { - var r = cache[pattern]; - if (r) { - return r; - } +/***/ }), +/* 811 */ +/***/ (function(module, exports, __webpack_require__) { - var replacers = negative ? NEGATIVE_REPLACERS : POSITIVE_REPLACERS; +"use strict"; - var source = replacers.reduce(function (prev, current) { - return prev.replace(current[0], current[1].bind(pattern)); - }, pattern); +const {Transform} = __webpack_require__(138); - return cache[pattern] = new RegExp(source, 'i'); +class ObjectTransform extends Transform { + constructor() { + super({ + objectMode: true + }); + } } -// Windows -// -------------------------------------------------------------- -/* istanbul ignore if */ -if ( -// Detect `process` so that it can run in browsers. -typeof process !== 'undefined' && (process.env && process.env.IGNORE_TEST_WIN32 || process.platform === 'win32')) { +class FilterStream extends ObjectTransform { + constructor(filter) { + super(); + this._filter = filter; + } - var filter = IgnoreBase.prototype._filter; - var make_posix = function make_posix(str) { - return (/^\\\\\?\\/.test(str) || /[^\x00-\x80]+/.test(str) ? str : str.replace(/\\/g, '/') - ); - }; + _transform(data, encoding, callback) { + if (this._filter(data)) { + this.push(data); + } - IgnoreBase.prototype._filter = function (path, slices) { - path = make_posix(path); - return filter.call(this, path, slices); - }; + callback(); + } } +class UniqueStream extends ObjectTransform { + constructor() { + super(); + this._pushed = new Set(); + } -/***/ }), -/* 782 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -module.exports = function (str) { - var isExtendedLengthPath = /^\\\\\?\\/.test(str); - var hasNonAscii = /[^\x00-\x80]+/.test(str); + _transform(data, encoding, callback) { + if (!this._pushed.has(data)) { + this.push(data); + this._pushed.add(data); + } - if (isExtendedLengthPath || hasNonAscii) { - return str; + callback(); } +} - return str.replace(/\\/g, '/'); +module.exports = { + FilterStream, + UniqueStream }; /***/ }), -/* 783 */ +/* 812 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md new file mode 100644 index 0000000000000..e22205540ef31 --- /dev/null +++ b/packages/kbn-server-route-repository/README.md @@ -0,0 +1,7 @@ +# @kbn/server-route-repository + +Utility functions for creating a typed server route repository, and a typed client, generating runtime validation and type validation from the same route definition. + +## Usage + +TBD diff --git a/src/legacy/server/jest.config.js b/packages/kbn-server-route-repository/jest.config.js similarity index 83% rename from src/legacy/server/jest.config.js rename to packages/kbn-server-route-repository/jest.config.js index 0a7322d2985fa..7449bb7cd3860 100644 --- a/src/legacy/server/jest.config.js +++ b/packages/kbn-server-route-repository/jest.config.js @@ -8,6 +8,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/server'], + rootDir: '../..', + roots: ['/packages/kbn-server-route-repository'], }; diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json new file mode 100644 index 0000000000000..ce1ca02d0c4f6 --- /dev/null +++ b/packages/kbn-server-route-repository/package.json @@ -0,0 +1,16 @@ +{ + "name": "@kbn/server-route-repository", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@kbn/io-ts-utils": "link:../kbn-io-ts-utils" + } +} diff --git a/packages/kbn-server-route-repository/src/create_server_route_factory.ts b/packages/kbn-server-route-repository/src/create_server_route_factory.ts new file mode 100644 index 0000000000000..edf9bd657f995 --- /dev/null +++ b/packages/kbn-server-route-repository/src/create_server_route_factory.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { + ServerRouteCreateOptions, + ServerRouteHandlerResources, + RouteParamsRT, + ServerRoute, +} from './typings'; + +export function createServerRouteFactory< + TRouteHandlerResources extends ServerRouteHandlerResources, + TRouteCreateOptions extends ServerRouteCreateOptions +>(): < + TEndpoint extends string, + TReturnType, + TRouteParamsRT extends RouteParamsRT | undefined = undefined +>( + route: ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions + > +) => ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions +> { + return (route) => route; +} diff --git a/packages/kbn-server-route-repository/src/create_server_route_repository.ts b/packages/kbn-server-route-repository/src/create_server_route_repository.ts new file mode 100644 index 0000000000000..5ac89ebcac77f --- /dev/null +++ b/packages/kbn-server-route-repository/src/create_server_route_repository.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { + ServerRouteHandlerResources, + ServerRouteRepository, + ServerRouteCreateOptions, +} from './typings'; + +export function createServerRouteRepository< + TRouteHandlerResources extends ServerRouteHandlerResources = never, + TRouteCreateOptions extends ServerRouteCreateOptions = never +>(): ServerRouteRepository { + let routes: Record = {}; + + return { + add(route) { + routes = { + ...routes, + [route.endpoint]: route, + }; + + return this as any; + }, + merge(repository) { + routes = { + ...routes, + ...Object.fromEntries(repository.getRoutes().map((route) => [route.endpoint, route])), + }; + + return this as any; + }, + getRoutes: () => Object.values(routes), + }; +} diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts new file mode 100644 index 0000000000000..08ef303ad0b3a --- /dev/null +++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { jsonRt } from '@kbn/io-ts-utils'; +import * as t from 'io-ts'; +import { decodeRequestParams } from './decode_request_params'; + +describe('decodeRequestParams', () => { + it('decodes request params', () => { + const decode = () => { + return decodeRequestParams( + { + params: { + serviceName: 'opbeans-java', + }, + body: null, + query: { + start: '', + }, + }, + t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + start: t.string, + }), + }) + ); + }; + expect(decode).not.toThrow(); + + expect(decode()).toEqual({ + path: { + serviceName: 'opbeans-java', + }, + query: { + start: '', + }, + }); + }); + + it('fails on excess keys', () => { + const decode = () => { + return decodeRequestParams( + { + params: { + serviceName: 'opbeans-java', + extraKey: '', + }, + body: null, + query: { + start: '', + }, + }, + t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + start: t.string, + }), + }) + ); + }; + + expect(decode).toThrowErrorMatchingInlineSnapshot(` + "Excess keys are not allowed: + path.extraKey" + `); + }); + + it('returns the decoded output', () => { + const decode = () => { + return decodeRequestParams( + { + params: {}, + query: { + _inspect: 'true', + }, + body: null, + }, + t.type({ + query: t.type({ + _inspect: jsonRt.pipe(t.boolean), + }), + }) + ); + }; + + expect(decode).not.toThrow(); + + expect(decode()).toEqual({ + query: { + _inspect: true, + }, + }); + }); + + it('strips empty params', () => { + const decode = () => { + return decodeRequestParams( + { + params: {}, + query: {}, + body: {}, + }, + t.type({ + body: t.any, + }) + ); + }; + + expect(decode).not.toThrow(); + + expect(decode()).toEqual({}); + }); +}); diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts new file mode 100644 index 0000000000000..00492d69b8ac5 --- /dev/null +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import * as t from 'io-ts'; +import { omitBy, isPlainObject, isEmpty } from 'lodash'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import Boom from '@hapi/boom'; +import { strictKeysRt } from '@kbn/io-ts-utils'; +import { RouteParamsRT } from './typings'; + +interface KibanaRequestParams { + body: unknown; + query: unknown; + params: unknown; +} + +export function decodeRequestParams( + params: KibanaRequestParams, + paramsRt: T +): t.OutputOf { + const paramMap = omitBy( + { + path: params.params, + body: params.body, + query: params.query, + }, + (val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val)) + ); + + // decode = validate + const result = strictKeysRt(paramsRt).decode(paramMap); + + if (isLeft(result)) { + throw Boom.badRequest(PathReporter.report(result)[0]); + } + + return result.right; +} diff --git a/packages/kbn-server-route-repository/src/format_request.ts b/packages/kbn-server-route-repository/src/format_request.ts new file mode 100644 index 0000000000000..49004a78ce0e0 --- /dev/null +++ b/packages/kbn-server-route-repository/src/format_request.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseEndpoint } from './parse_endpoint'; + +export function formatRequest(endpoint: string, pathParams: Record = {}) { + const { method, pathname: rawPathname } = parseEndpoint(endpoint); + + // replace template variables with path params + const pathname = Object.keys(pathParams).reduce((acc, paramName) => { + return acc.replace(`{${paramName}}`, pathParams[paramName]); + }, rawPathname); + + return { method, pathname }; +} diff --git a/packages/kbn-server-route-repository/src/index.ts b/packages/kbn-server-route-repository/src/index.ts new file mode 100644 index 0000000000000..23621c5b213bc --- /dev/null +++ b/packages/kbn-server-route-repository/src/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createServerRouteRepository } from './create_server_route_repository'; +export { createServerRouteFactory } from './create_server_route_factory'; +export { formatRequest } from './format_request'; +export { parseEndpoint } from './parse_endpoint'; +export { decodeRequestParams } from './decode_request_params'; +export { routeValidationObject } from './route_validation_object'; +export { + RouteRepositoryClient, + ReturnOf, + EndpointOf, + ClientRequestParamsOf, + DecodedRequestParamsOf, + ServerRouteRepository, + ServerRoute, + RouteParamsRT, +} from './typings'; diff --git a/packages/kbn-server-route-repository/src/parse_endpoint.ts b/packages/kbn-server-route-repository/src/parse_endpoint.ts new file mode 100644 index 0000000000000..fd40489b0f4a5 --- /dev/null +++ b/packages/kbn-server-route-repository/src/parse_endpoint.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +type Method = 'get' | 'post' | 'put' | 'delete'; + +export function parseEndpoint(endpoint: string) { + const parts = endpoint.split(' '); + + const method = parts[0].trim().toLowerCase() as Method; + const pathname = parts[1].trim(); + + if (!['get', 'post', 'put', 'delete'].includes(method)) { + throw new Error('Endpoint was not prefixed with a valid HTTP method'); + } + + return { method, pathname }; +} diff --git a/packages/kbn-server-route-repository/src/route_validation_object.ts b/packages/kbn-server-route-repository/src/route_validation_object.ts new file mode 100644 index 0000000000000..550be8d20d446 --- /dev/null +++ b/packages/kbn-server-route-repository/src/route_validation_object.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { schema } from '@kbn/config-schema'; + +const anyObject = schema.object({}, { unknowns: 'allow' }); + +export const routeValidationObject = { + // `body` can be null, but `validate` expects non-nullable types + // if any validation is defined. Not having validation currently + // means we don't get the payload. See + // https://github.com/elastic/kibana/issues/50179 + body: schema.nullable(anyObject), + params: anyObject, + query: anyObject, +}; diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts new file mode 100644 index 0000000000000..c9015e19b82f8 --- /dev/null +++ b/packages/kbn-server-route-repository/src/test_types.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import * as t from 'io-ts'; +import { createServerRouteRepository } from './create_server_route_repository'; +import { decodeRequestParams } from './decode_request_params'; +import { EndpointOf, ReturnOf, RouteRepositoryClient } from './typings'; + +function assertType(value: TShape) { + return value; +} + +// Generic arguments for createServerRouteRepository should be set, +// if not, registering routes should not be allowed +createServerRouteRepository().add({ + // @ts-expect-error + endpoint: 'any_endpoint', + // @ts-expect-error + handler: async ({ params }) => {}, +}); + +// If a params codec is not set, its type should not be available in +// the request handler. +createServerRouteRepository<{}, {}>().add({ + endpoint: 'endpoint_without_params', + handler: async (resources) => { + // @ts-expect-error Argument of type '{}' is not assignable to parameter of type '{ params: any; }'. + assertType<{ params: any }>(resources); + }, +}); + +// If a params codec is set, its type _should_ be available in the +// request handler. +createServerRouteRepository<{}, {}>().add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + handler: async (resources) => { + assertType<{ params: { path: { serviceName: string } } }>(resources); + }, +}); + +// Resources should be passed to the request handler. +createServerRouteRepository<{ context: { getSpaceId: () => string } }, {}>().add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + handler: async ({ context }) => { + const spaceId = context.getSpaceId(); + assertType(spaceId); + }, +}); + +// Create options are available when registering a route. +createServerRouteRepository<{}, { options: { tags: string[] } }>().add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + options: { + tags: [], + }, + handler: async (resources) => { + assertType<{ params: { path: { serviceName: string } } }>(resources); + }, +}); + +const repository = createServerRouteRepository<{}, {}>() + .add({ + endpoint: 'endpoint_without_params', + handler: async () => { + return { + noParamsForMe: true, + }; + }, + }) + .add({ + endpoint: 'endpoint_with_params', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + handler: async () => { + return { + yesParamsForMe: true, + }; + }, + }) + .add({ + endpoint: 'endpoint_with_optional_params', + params: t.partial({ + query: t.partial({ + serviceName: t.string, + }), + }), + handler: async () => { + return { + someParamsForMe: true, + }; + }, + }); + +type TestRepository = typeof repository; + +// EndpointOf should return all valid endpoints of a repository + +assertType>>([ + 'endpoint_with_params', + 'endpoint_without_params', + 'endpoint_with_optional_params', +]); + +// @ts-expect-error Type '"this_endpoint_does_not_exist"' is not assignable to type '"endpoint_without_params" | "endpoint_with_params" | "endpoint_with_optional_params"' +assertType>>(['this_endpoint_does_not_exist']); + +// ReturnOf should return the return type of a request handler. + +assertType>({ + noParamsForMe: true, +}); + +const noParamsInvalid: ReturnOf = { + // @ts-expect-error type '{ paramsForMe: boolean; }' is not assignable to type '{ noParamsForMe: boolean; }'. + paramsForMe: true, +}; + +// RouteRepositoryClient + +type TestClient = RouteRepositoryClient; + +const client: TestClient = {} as any; + +// It should respect any additional create options. + +// @ts-expect-error Property 'timeout' is missing +client({ + endpoint: 'endpoint_without_params', +}); + +client({ + endpoint: 'endpoint_without_params', + timeout: 1, +}); + +// It does not allow params for routes without a params codec +client({ + endpoint: 'endpoint_without_params', + // @ts-expect-error Object literal may only specify known properties, and 'params' does not exist in type + params: {}, + timeout: 1, +}); + +// It requires params for routes with a params codec +client({ + endpoint: 'endpoint_with_params', + params: { + // @ts-expect-error property 'serviceName' is missing in type '{}' + path: {}, + }, + timeout: 1, +}); + +// Params are optional if the codec has no required keys +client({ + endpoint: 'endpoint_with_optional_params', + timeout: 1, +}); + +// If optional, an error will still occur if the params do not match +client({ + endpoint: 'endpoint_with_optional_params', + timeout: 1, + params: { + // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type + path: '', + }, +}); + +// The return type is correctly inferred +client({ + endpoint: 'endpoint_with_params', + params: { + path: { + serviceName: '', + }, + }, + timeout: 1, +}).then((res) => { + assertType<{ + noParamsForMe: boolean; + // @ts-expect-error Property 'noParamsForMe' is missing in type + }>(res); + + assertType<{ + yesParamsForMe: boolean; + }>(res); +}); + +// decodeRequestParams should return the type of the codec that is passed +assertType<{ path: { serviceName: string } }>( + decodeRequestParams( + { + params: { + serviceName: 'serviceName', + }, + body: undefined, + query: undefined, + }, + t.type({ path: t.type({ serviceName: t.string }) }) + ) +); + +assertType<{ path: { serviceName: boolean } }>( + // @ts-expect-error The types of 'path.serviceName' are incompatible between these types. + decodeRequestParams( + { + params: { + serviceName: 'serviceName', + }, + body: undefined, + query: undefined, + }, + t.type({ path: t.type({ serviceName: t.string }) }) + ) +); diff --git a/packages/kbn-server-route-repository/src/typings.ts b/packages/kbn-server-route-repository/src/typings.ts new file mode 100644 index 0000000000000..c27f67c71e88b --- /dev/null +++ b/packages/kbn-server-route-repository/src/typings.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { RequiredKeys } from 'utility-types'; + +type MaybeOptional }> = RequiredKeys< + T['params'] +> extends never + ? { params?: T['params'] } + : { params: T['params'] }; + +type WithoutIncompatibleMethods = Omit & { + encode: t.Encode; + asEncoder: () => t.Encoder; +}; + +export type RouteParamsRT = WithoutIncompatibleMethods< + t.Type<{ + path?: any; + query?: any; + body?: any; + }> +>; + +export interface RouteState { + [endpoint: string]: ServerRoute; +} + +export type ServerRouteHandlerResources = Record; +export type ServerRouteCreateOptions = Record; + +export type ServerRoute< + TEndpoint extends string, + TRouteParamsRT extends RouteParamsRT | undefined, + TRouteHandlerResources extends ServerRouteHandlerResources, + TReturnType, + TRouteCreateOptions extends ServerRouteCreateOptions +> = { + endpoint: TEndpoint; + params?: TRouteParamsRT; + handler: ({}: TRouteHandlerResources & + (TRouteParamsRT extends RouteParamsRT + ? DecodedRequestParamsOfType + : {})) => Promise; +} & TRouteCreateOptions; + +export interface ServerRouteRepository< + TRouteHandlerResources extends ServerRouteHandlerResources = ServerRouteHandlerResources, + TRouteCreateOptions extends ServerRouteCreateOptions = ServerRouteCreateOptions, + TRouteState extends RouteState = RouteState +> { + add< + TEndpoint extends string, + TReturnType, + TRouteParamsRT extends RouteParamsRT | undefined = undefined + >( + route: ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions + > + ): ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions, + TRouteState & + { + [key in TEndpoint]: ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + TReturnType, + TRouteCreateOptions + >; + } + >; + merge< + TServerRouteRepository extends ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions + > + >( + repository: TServerRouteRepository + ): TServerRouteRepository extends ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions, + infer TRouteStateToMerge + > + ? ServerRouteRepository< + TRouteHandlerResources, + TRouteCreateOptions, + TRouteState & TRouteStateToMerge + > + : never; + getRoutes: () => Array< + ServerRoute + >; +} + +type ClientRequestParamsOfType< + TRouteParamsRT extends RouteParamsRT +> = TRouteParamsRT extends t.Mixed + ? MaybeOptional<{ + params: t.OutputOf; + }> + : {}; + +type DecodedRequestParamsOfType< + TRouteParamsRT extends RouteParamsRT +> = TRouteParamsRT extends t.Mixed + ? MaybeOptional<{ + params: t.TypeOf; + }> + : {}; + +export type EndpointOf< + TServerRouteRepository extends ServerRouteRepository +> = TServerRouteRepository extends ServerRouteRepository + ? keyof TRouteState + : never; + +export type ReturnOf< + TServerRouteRepository extends ServerRouteRepository, + TEndpoint extends EndpointOf +> = TServerRouteRepository extends ServerRouteRepository + ? TEndpoint extends keyof TRouteState + ? TRouteState[TEndpoint] extends ServerRoute< + any, + any, + any, + infer TReturnType, + ServerRouteCreateOptions + > + ? TReturnType + : never + : never + : never; + +export type DecodedRequestParamsOf< + TServerRouteRepository extends ServerRouteRepository, + TEndpoint extends EndpointOf +> = TServerRouteRepository extends ServerRouteRepository + ? TEndpoint extends keyof TRouteState + ? TRouteState[TEndpoint] extends ServerRoute< + any, + infer TRouteParamsRT, + any, + any, + ServerRouteCreateOptions + > + ? TRouteParamsRT extends RouteParamsRT + ? DecodedRequestParamsOfType + : {} + : never + : never + : never; + +export type ClientRequestParamsOf< + TServerRouteRepository extends ServerRouteRepository, + TEndpoint extends EndpointOf +> = TServerRouteRepository extends ServerRouteRepository + ? TEndpoint extends keyof TRouteState + ? TRouteState[TEndpoint] extends ServerRoute< + any, + infer TRouteParamsRT, + any, + any, + ServerRouteCreateOptions + > + ? TRouteParamsRT extends RouteParamsRT + ? ClientRequestParamsOfType + : {} + : never + : never + : never; + +export type RouteRepositoryClient< + TServerRouteRepository extends ServerRouteRepository, + TAdditionalClientOptions extends Record +> = >( + options: { + endpoint: TEndpoint; + } & ClientRequestParamsOf & + TAdditionalClientOptions +) => Promise>; diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json new file mode 100644 index 0000000000000..8f1e72172c675 --- /dev/null +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-server-route-repository/src", + "types": [ + "jest", + "node" + ], + "noUnusedLocals": false + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 5c81c9100e601..db10d4630ff9c 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.json", "compilerOptions": { "incremental": false, "outDir": "target", diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts index 5d946b73d9759..f55a9aa80d40d 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts @@ -7,7 +7,9 @@ */ export { ErrorReporter } from './error_reporter'; -export { TaskContext, createTaskContext } from './task_context'; + +export type { TaskContext } from './task_context'; +export { createTaskContext } from './task_context'; export { parseConfigsTask } from './parse_configs_task'; export { extractCollectorsTask } from './extract_collectors_task'; diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 39946fe9907e5..419af1d02f83b 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -6,7 +6,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", + "isolatedModules": true }, "include": [ "src/**/*", diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 8ef11e2dba462..63eca93def64d 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -11,6 +11,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { run, createFailError, createFlagError } from '@kbn/dev-utils'; import globby from 'globby'; +import normalize from 'normalize-path'; import { getFailures, TestFailure } from './get_failures'; import { GithubApi, GithubIssueMini } from './github_api'; @@ -61,7 +62,9 @@ export function runFailedTestsReporterCli() { throw createFlagError('Missing --build-url or process.env.BUILD_URL'); } - const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS; + const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => + normalize(Path.resolve(p)) + ); log.info('Searching for reports at', patterns); const reportPaths = await globby(patterns, { absolute: true, diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js index abbf911cfc8fc..a9ec32023f2bf 100644 --- a/packages/kbn-ui-shared-deps/polyfills.js +++ b/packages/kbn-ui-shared-deps/polyfills.js @@ -8,7 +8,6 @@ require('core-js/stable'); require('regenerator-runtime/runtime'); -require('custom-event-polyfill'); if (typeof window.Event === 'object') { // IE11 doesn't support unknown event types, required by react-use @@ -17,6 +16,4 @@ if (typeof window.Event === 'object') { } require('whatwg-fetch'); -require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); -require('./vendor/childnode_remove_polyfill'); require('symbol-observable'); diff --git a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js deleted file mode 100644 index d8818fe809ccb..0000000000000 --- a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -/* @notice - * This product bundles childnode-remove which is available under a - * "MIT" license. - * - * The MIT License (MIT) - * - * Copyright (c) 2016-present, jszhou - * https://github.com/jserz/js_piece - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* eslint-disable */ - -(function (arr) { - arr.forEach(function (item) { - if (item.hasOwnProperty('remove')) { - return; - } - Object.defineProperty(item, 'remove', { - configurable: true, - enumerable: true, - writable: true, - value: function remove() { - if (this.parentNode !== null) - this.parentNode.removeChild(this); - } - }); - }); -})([Element.prototype, CharacterData.prototype, DocumentType.prototype]); diff --git a/packages/kbn-utils/src/package_json/index.ts b/packages/kbn-utils/src/package_json/index.ts index 40ce353780749..d9304cee2ca38 100644 --- a/packages/kbn-utils/src/package_json/index.ts +++ b/packages/kbn-utils/src/package_json/index.ts @@ -14,3 +14,7 @@ export const kibanaPackageJson = { __dirname: dirname(resolve(REPO_ROOT, 'package.json')), ...require(resolve(REPO_ROOT, 'package.json')), }; + +export const isKibanaDistributable = () => { + return kibanaPackageJson.build && kibanaPackageJson.build.distributable === true; +}; diff --git a/src/cli/cli.js b/src/cli/cli.js index 4540bf4a3f93c..d3bff4f492a80 100644 --- a/src/cli/cli.js +++ b/src/cli/cli.js @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Command from './command'; import serveCommand from './serve/serve'; diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index a494e4538e79a..ad83965efde33 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -12,8 +12,7 @@ import { statSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; -import { getConfigPath, fromRoot } from '@kbn/utils'; -import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; +import { getConfigPath, fromRoot, isKibanaDistributable } from '@kbn/utils'; import { readKeystore } from '../keystore/read_keystore'; function canRequire(path) { @@ -65,9 +64,10 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete rawConfig.xpack; } - if (opts.dev) { - set('env', 'development'); + // only used to set cliArgs.envName, we don't want to inject that into the config + delete extraCliOptions.env; + if (opts.dev) { if (!has('elasticsearch.username')) { set('elasticsearch.username', 'kibana_system'); } @@ -184,7 +184,7 @@ export default function (program) { .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) .option('--optimize', 'Deprecated, running the optimizer is no longer required'); - if (!IS_KIBANA_DISTRIBUTABLE) { + if (!isKibanaDistributable()) { command .option('--oss', 'Start Kibana without X-Pack') .option( diff --git a/src/cli_encryption_keys/cli_encryption_keys.js b/src/cli_encryption_keys/cli_encryption_keys.js index e922b9354d291..acee81aabb706 100644 --- a/src/cli_encryption_keys/cli_encryption_keys.js +++ b/src/cli_encryption_keys/cli_encryption_keys.js @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; + import Command from '../cli/command'; import { EncryptionConfig } from './encryption_config'; diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index b325f685766aa..9f44e5d56e9d2 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -7,8 +7,8 @@ */ import _ from 'lodash'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; -import { pkg } from '../core/server/utils'; import Command from '../cli/command'; import { Keystore } from '../cli/keystore'; diff --git a/src/cli_plugin/cli.js b/src/cli_plugin/cli.js index 24ccba6a23397..5ef142192c509 100644 --- a/src/cli_plugin/cli.js +++ b/src/cli_plugin/cli.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Command from '../cli/command'; import { listCommand } from './list'; import { installCommand } from './install'; diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index c028facc28e2b..2683dd41d2bb3 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { getConfigPath } from '@kbn/utils'; -import { pkg } from '../../core/server/utils'; +import { getConfigPath, kibanaPackageJson as pkg } from '@kbn/utils'; import { install } from './install'; import { Logger } from '../lib/logger'; import { parse, parseMilliseconds } from './settings'; diff --git a/src/cli_plugin/install/kibana.js b/src/cli_plugin/install/kibana.js index 29cb8df7401b6..1de157b951d03 100644 --- a/src/cli_plugin/install/kibana.js +++ b/src/cli_plugin/install/kibana.js @@ -9,7 +9,7 @@ import path from 'path'; import { statSync } from 'fs'; -import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; +import { versionSatisfies, cleanVersion } from './utils/version'; export function existingInstall(settings, logger) { try { diff --git a/src/cli_plugin/install/settings.js b/src/cli_plugin/install/settings.js index 94473cc12aab2..e1536d66e0529 100644 --- a/src/cli_plugin/install/settings.js +++ b/src/cli_plugin/install/settings.js @@ -7,10 +7,8 @@ */ import { resolve } from 'path'; - import expiry from 'expiry-js'; - -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; function generateUrls({ version, plugin }) { return [ diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index f06fd7eca7902..c7985763524ed 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -7,8 +7,8 @@ */ import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { fromRoot } from '@kbn/utils'; -import { fromRoot } from '../../core/server/utils'; import { parseMilliseconds, parse } from './settings'; const SECOND = 1000; diff --git a/src/legacy/utils/version.js b/src/cli_plugin/install/utils/version.js similarity index 100% rename from src/legacy/utils/version.js rename to src/cli_plugin/install/utils/version.js diff --git a/src/cli_plugin/list/index.js b/src/cli_plugin/list/index.js index ce55b939b8a4c..02d1ed19f8445 100644 --- a/src/cli_plugin/list/index.js +++ b/src/cli_plugin/list/index.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; import { list } from './list'; import { Logger } from '../lib/logger'; import { logWarnings } from '../lib/log_warnings'; diff --git a/src/cli_plugin/remove/settings.js b/src/cli_plugin/remove/settings.js index 333fa7cb0f2e1..2381770ee0a65 100644 --- a/src/cli_plugin/remove/settings.js +++ b/src/cli_plugin/remove/settings.js @@ -7,8 +7,7 @@ */ import { resolve } from 'path'; - -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; export function parse(command, options) { const settings = { diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a946640f58b0d..b179c998f1126 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -216,6 +216,7 @@ export class DocLinksService { }, maps: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, + importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index de13785a17f5b..ed2d9bc0b3917 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -11,6 +11,16 @@ min-height: 100%; } +#app-fixed-viewport { + pointer-events: none; + visibility: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + .app-wrapper { display: flex; flex-flow: column nowrap; @@ -35,6 +45,10 @@ @mixin kbnAffordForHeader($headerHeight) { padding-top: $headerHeight; + #app-fixed-viewport { + top: $headerHeight; + } + .euiFlyout, .euiCollapsibleNav { top: $headerHeight; diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 843f2a253f33e..787fa475c7d5f 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -52,6 +52,7 @@ export class RenderingService { {chromeHeader}
+
{bannerComponent}
{appComponent}
diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 4a07e0c010685..a2267635e86f2 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -83,6 +83,11 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot try { await root.setup(); await root.start(); + + // notify parent process know when we are ready for dev mode. + if (process.send) { + process.send(['SERVER_LISTENING']); + } } catch (err) { await shutdown(err); } diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index e3c236405a596..a8063c317b3c5 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -6,27 +6,12 @@ * Side Public License, v 1. */ -import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; +import { getDeprecationsForGlobalSettings } from '../test_utils'; import { coreDeprecationProvider } from './core_deprecations'; - const initialEnv = { ...process.env }; -const applyCoreDeprecations = (settings: Record = {}) => { - const deprecations = coreDeprecationProvider(configDeprecationFactory); - const deprecationMessages: string[] = []; - const migrated = applyDeprecations( - settings, - deprecations.map((deprecation) => ({ - deprecation, - path: '', - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyCoreDeprecations = (settings?: Record) => + getDeprecationsForGlobalSettings({ provider: coreDeprecationProvider, settings }); describe('core deprecations', () => { beforeEach(() => { diff --git a/src/core/server/config/ensure_valid_configuration.test.ts b/src/core/server/config/ensure_valid_configuration.test.ts new file mode 100644 index 0000000000000..474e8dd59b4c4 --- /dev/null +++ b/src/core/server/config/ensure_valid_configuration.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { configServiceMock } from './mocks'; +import { ensureValidConfiguration } from './ensure_valid_configuration'; +import { CriticalError } from '../errors'; + +describe('ensureValidConfiguration', () => { + let configService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + configService = configServiceMock.create(); + configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic'])); + }); + + it('returns normally when there is no unused keys', async () => { + configService.getUnusedPaths.mockResolvedValue([]); + await expect(ensureValidConfiguration(configService as any)).resolves.toBeUndefined(); + }); + + it('throws when there are some unused keys', async () => { + configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']); + + await expect(ensureValidConfiguration(configService as any)).rejects.toMatchInlineSnapshot( + `[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]` + ); + }); + + it('throws a `CriticalError` with the correct processExitCode value', async () => { + expect.assertions(2); + + configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']); + + try { + await ensureValidConfiguration(configService as any); + } catch (e) { + expect(e).toBeInstanceOf(CriticalError); + expect(e.processExitCode).toEqual(64); + } + }); +}); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/config/ensure_valid_configuration.ts similarity index 62% rename from src/core/server/legacy/config/ensure_valid_configuration.ts rename to src/core/server/config/ensure_valid_configuration.ts index fd3dd29e3d354..a33625cc0841d 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/config/ensure_valid_configuration.ts @@ -6,20 +6,13 @@ * Side Public License, v 1. */ -import { getUnusedConfigKeys } from './get_unused_config_keys'; -import { ConfigService } from '../../config'; -import { CriticalError } from '../../errors'; -import { LegacyServiceSetupConfig } from '../types'; +import { ConfigService } from '@kbn/config'; +import { CriticalError } from '../errors'; -export async function ensureValidConfiguration( - configService: ConfigService, - { legacyConfig, settings }: LegacyServiceSetupConfig -) { - const unusedConfigKeys = await getUnusedConfigKeys({ - coreHandledConfigPaths: await configService.getUsedPaths(), - settings, - legacyConfig, - }); +export async function ensureValidConfiguration(configService: ConfigService) { + await configService.validate(); + + const unusedConfigKeys = await configService.getUnusedPaths(); if (unusedConfigKeys.length > 0) { const message = `Unknown configuration key(s): ${unusedConfigKeys diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index b1086d4470335..686564c6d678a 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -7,6 +7,7 @@ */ export { coreDeprecationProvider } from './deprecation'; +export { ensureValidConfiguration } from './ensure_valid_configuration'; export { ConfigService, diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts new file mode 100644 index 0000000000000..2eaf462768724 --- /dev/null +++ b/src/core/server/config/test_utils.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { ConfigDeprecationProvider } from '@kbn/config'; +import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; + +function collectDeprecations( + provider: ConfigDeprecationProvider, + settings: Record, + path: string +) { + const deprecations = provider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map((deprecation) => ({ + deprecation, + path, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +} + +export const getDeprecationsFor = ({ + provider, + settings = {}, + path, +}: { + provider: ConfigDeprecationProvider; + settings?: Record; + path: string; +}) => { + return collectDeprecations(provider, { [path]: settings }, path); +}; + +export const getDeprecationsForGlobalSettings = ({ + provider, + settings = {}, +}: { + provider: ConfigDeprecationProvider; + settings?: Record; +}) => { + return collectDeprecations(provider, settings, ''); +}; diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts index d51c369146957..830f4a9a94364 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts @@ -10,7 +10,7 @@ import { registerRouteForBundleMock } from './register_bundle_routes.test.mocks' import { PackageInfo } from '@kbn/config'; import { httpServiceMock } from '../../http/http_service.mock'; -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { registerBundleRoutes } from './register_bundle_routes'; import { FileHashCache } from './file_hash_cache'; @@ -29,9 +29,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({ internal: ids.reduce((map, id) => { map.set(id, { publicTargetDir: `/plugins/${id}/public-target-dir`, + publicAssetsDir: `/plugins/${id}/public-assets-dir`, + version: '8.0.0', + requiredBundles: [], }); return map; - }, new Map()), + }, new Map()), }); describe('registerBundleRoutes', () => { @@ -86,16 +89,16 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-a/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-a/', - routePath: '/42/bundles/plugin/plugin-a/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/', + routePath: '/42/bundles/plugin/plugin-a/8.0.0/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-b/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-b/', - routePath: '/42/bundles/plugin/plugin-b/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/', + routePath: '/42/bundles/plugin/plugin-b/8.0.0/', }); }); }); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index ee54f8ef34622..f313f10003631 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -8,10 +8,10 @@ import { join } from 'path'; import { PackageInfo } from '@kbn/config'; +import { fromRoot } from '@kbn/utils'; import { distDir as uiSharedDepsDistDir } from '@kbn/ui-shared-deps'; import { IRouter } from '../../http'; import { UiPlugins } from '../../plugins'; -import { fromRoot } from '../../utils'; import { FileHashCache } from './file_hash_cache'; import { registerRouteForBundle } from './bundles_route'; @@ -27,7 +27,7 @@ import { registerRouteForBundle } from './bundles_route'; */ export function registerBundleRoutes({ router, - serverBasePath, // serverBasePath + serverBasePath, uiPlugins, packageInfo, }: { @@ -57,10 +57,10 @@ export function registerBundleRoutes({ isDist, }); - [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir }]) => { + [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => { registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/`, - routePath: `/${buildNum}/bundles/plugin/${id}/`, + publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`, + routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`, bundlesPath: publicTargetDir, fileHashCache, isDist, diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index dac941767ebb5..bc1098832bac5 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -7,9 +7,11 @@ */ import Path from 'path'; +import { stringify } from 'querystring'; import { Env } from '@kbn/config'; +import { schema } from '@kbn/config-schema'; +import { fromRoot } from '@kbn/utils'; -import { fromRoot } from '../utils'; import { InternalCoreSetup } from '../internal_types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; @@ -49,6 +51,41 @@ export class CoreApp { }); }); + // remove trailing slash catch-all + router.get( + { + path: '/{path*}', + validate: { + params: schema.object({ + path: schema.maybe(schema.string()), + }), + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + }, + }, + async (context, req, res) => { + const { query, params } = req; + const { path } = params; + if (!path || !path.endsWith('/')) { + return res.notFound(); + } + + const basePath = httpSetup.basePath.get(req); + let rewrittenPath = path.slice(0, -1); + if (`/${path}`.startsWith(basePath)) { + rewrittenPath = rewrittenPath.substring(basePath.length); + } + + const querystring = query ? stringify(query) : undefined; + const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`; + + return res.redirected({ + headers: { + location: url, + }, + }); + } + ); + router.get({ path: '/core', validate: false }, async (context, req, res) => res.ok({ body: { version: '0.0.1' } }) ); diff --git a/src/core/server/core_app/integration_tests/core_app_routes.test.ts b/src/core/server/core_app/integration_tests/core_app_routes.test.ts new file mode 100644 index 0000000000000..6b0643f7d1bc7 --- /dev/null +++ b/src/core/server/core_app/integration_tests/core_app_routes.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as kbnTestServer from '../../../test_helpers/kbn_server'; +import { Root } from '../../root'; + +describe('Core app routes', () => { + let root: Root; + + beforeAll(async function () { + root = kbnTestServer.createRoot({ + plugins: { initialize: false }, + server: { + basePath: '/base-path', + }, + }); + + await root.setup(); + await root.start(); + }); + + afterAll(async function () { + await root.shutdown(); + }); + + describe('`/{path*}` route', () => { + it('redirects requests to include the basePath', async () => { + const response = await kbnTestServer.request.get(root, '/some-path/').expect(302); + expect(response.get('location')).toEqual('/base-path/some-path'); + }); + + it('includes the query in the redirect', async () => { + const response = await kbnTestServer.request.get(root, '/some-path/?foo=bar').expect(302); + expect(response.get('location')).toEqual('/base-path/some-path?foo=bar'); + }); + + it('does not redirect if the path does not end with `/`', async () => { + await kbnTestServer.request.get(root, '/some-path').expect(404); + }); + + it('does not add the basePath if the path already contains it', async () => { + const response = await kbnTestServer.request.get(root, '/base-path/foo/').expect(302); + expect(response.get('location')).toEqual('/base-path/foo'); + }); + }); + + describe('`/` route', () => { + it('prevails on the `/{path*}` route', async () => { + const response = await kbnTestServer.request.get(root, '/').expect(302); + expect(response.get('location')).toEqual('/base-path/app/home'); + }); + }); +}); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 23b804b535405..f8ef1a7a20a83 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -12,29 +12,17 @@ import { mockReadPkcs12Truststore, } from './elasticsearch_config.test.mocks'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; import { ElasticsearchConfig, config } from './elasticsearch_config'; +import { getDeprecationsFor } from '../config/test_utils'; const CONFIG_PATH = 'elasticsearch'; -const applyElasticsearchDeprecations = (settings: Record = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config: any = {}; - _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyElasticsearchDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: config.deprecations!, + settings, + path: CONFIG_PATH, + }); test('set correct defaults', () => { const configValue = new ElasticsearchConfig(config.schema.validate({})); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 356dad201ce95..daf7424b8f8bd 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -11,6 +11,7 @@ import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; import { hostname } from 'os'; import url from 'url'; +import { ServiceConfigDescriptor } from '../internal_types'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url'; @@ -20,141 +21,143 @@ const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; -// before update to make sure it's in sync with validation rules in Legacy -// https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js -export const config = { - path: 'server' as const, - schema: schema.object( - { - name: schema.string({ defaultValue: () => hostname() }), - autoListen: schema.boolean({ defaultValue: true }), - publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), - basePath: schema.maybe( - schema.string({ - validate: match(validBasePathRegex, "must start with a slash, don't end with one"), - }) - ), - cors: schema.object( - { - enabled: schema.boolean({ defaultValue: false }), - allowCredentials: schema.boolean({ defaultValue: false }), - allowOrigin: schema.oneOf( - [ - schema.arrayOf(hostURISchema, { minSize: 1 }), - schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), - ], - { - defaultValue: ['*'], - } - ), +const configSchema = schema.object( + { + name: schema.string({ defaultValue: () => hostname() }), + autoListen: schema.boolean({ defaultValue: true }), + publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + basePath: schema.maybe( + schema.string({ + validate: match(validBasePathRegex, "must start with a slash, don't end with one"), + }) + ), + cors: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + allowCredentials: schema.boolean({ defaultValue: false }), + allowOrigin: schema.oneOf( + [ + schema.arrayOf(hostURISchema, { minSize: 1 }), + schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), + ], + { + defaultValue: ['*'], + } + ), + }, + { + validate(value) { + if (value.allowCredentials === true && value.allowOrigin.includes('*')) { + return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; + } }, - { - validate(value) { - if (value.allowCredentials === true && value.allowOrigin.includes('*')) { - return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; - } - }, + } + ), + customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { + defaultValue: {}, + }), + host: schema.string({ + defaultValue: 'localhost', + hostname: true, + validate(value) { + if (value === '0') { + return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; } + }, + }), + maxPayload: schema.byteSize({ + defaultValue: '1048576b', + }), + port: schema.number({ + defaultValue: 5601, + }), + rewriteBasePath: schema.boolean({ defaultValue: false }), + ssl: sslSchema, + keepaliveTimeout: schema.number({ + defaultValue: 120000, + }), + socketTimeout: schema.number({ + defaultValue: 120000, + }), + compression: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + referrerWhitelist: schema.maybe( + schema.arrayOf( + schema.string({ + hostname: true, + }), + { minSize: 1 } + ) + ), + }), + uuid: schema.maybe( + schema.string({ + validate: match(uuidRegexp, 'must be a valid uuid'), + }) + ), + xsrf: schema.object({ + disableProtection: schema.boolean({ defaultValue: false }), + allowlist: schema.arrayOf( + schema.string({ validate: match(/^\//, 'must start with a slash') }), + { defaultValue: [] } ), - customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { - defaultValue: {}, - }), - host: schema.string({ - defaultValue: 'localhost', - hostname: true, + }), + requestId: schema.object( + { + allowFromAnyIp: schema.boolean({ defaultValue: false }), + ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), + }, + { validate(value) { - if (value === '0') { - return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; + if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { + return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; } }, - }), - maxPayload: schema.byteSize({ - defaultValue: '1048576b', - }), - port: schema.number({ - defaultValue: 5601, - }), - rewriteBasePath: schema.boolean({ defaultValue: false }), - ssl: sslSchema, - keepaliveTimeout: schema.number({ - defaultValue: 120000, - }), - socketTimeout: schema.number({ - defaultValue: 120000, - }), - compression: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - referrerWhitelist: schema.maybe( - schema.arrayOf( - schema.string({ - hostname: true, - }), - { minSize: 1 } - ) - ), - }), - uuid: schema.maybe( - schema.string({ - validate: match(uuidRegexp, 'must be a valid uuid'), - }) - ), - xsrf: schema.object({ - disableProtection: schema.boolean({ defaultValue: false }), - allowlist: schema.arrayOf( - schema.string({ validate: match(/^\//, 'must start with a slash') }), - { defaultValue: [] } - ), - }), - requestId: schema.object( - { - allowFromAnyIp: schema.boolean({ defaultValue: false }), - ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), - }, - { - validate(value) { - if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { - return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; - } - }, + } + ), + }, + { + validate: (rawConfig) => { + if (!rawConfig.basePath && rawConfig.rewriteBasePath) { + return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + } + + if (rawConfig.publicBaseUrl) { + const parsedUrl = url.parse(rawConfig.publicBaseUrl); + if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { + return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; } - ), - }, - { - validate: (rawConfig) => { - if (!rawConfig.basePath && rawConfig.rewriteBasePath) { - return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { + return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; } + } - if (rawConfig.publicBaseUrl) { - const parsedUrl = url.parse(rawConfig.publicBaseUrl); - if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { - return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; - } - if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { - return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; - } - } + if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { + return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; + } - if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { - return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; - } + if ( + rawConfig.ssl.enabled && + rawConfig.ssl.redirectHttpFromPort !== undefined && + rawConfig.ssl.redirectHttpFromPort === rawConfig.port + ) { + return ( + 'Kibana does not accept http traffic to [port] when ssl is ' + + 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + + `cannot be configured to the same value. Both are [${rawConfig.port}].` + ); + } + }, + } +); - if ( - rawConfig.ssl.enabled && - rawConfig.ssl.redirectHttpFromPort !== undefined && - rawConfig.ssl.redirectHttpFromPort === rawConfig.port - ) { - return ( - 'Kibana does not accept http traffic to [port] when ssl is ' + - 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + - `cannot be configured to the same value. Both are [${rawConfig.port}].` - ); - } - }, - } - ), +export type HttpConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { + path: 'server' as const, + schema: configSchema, + deprecations: ({ rename }) => [rename('maxPayloadBytes', 'maxPayload')], }; -export type HttpConfigType = TypeOf; export class HttpConfig implements IHttpConfig { public name: string; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index af358caae8bfc..5433f0d3c3e31 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -12,8 +12,6 @@ import { legacyClusterClientInstanceMock, } from './core_service.test.mocks'; -import Boom from '@hapi/boom'; -import { Request } from '@hapi/hapi'; import { errors as esErrors } from 'elasticsearch'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; @@ -22,16 +20,6 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalElasticsearchServiceStart } from '../../elasticsearch'; -interface User { - id: string; - roles?: string[]; -} - -interface StorageData { - value: User; - expires: number; -} - const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', @@ -197,172 +185,6 @@ describe('http service', () => { }); }); - describe('legacy server', () => { - describe('#registerAuth()', () => { - const sessionDurationMs = 1000; - - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - }, 30000); - - afterEach(async () => { - MockLegacyScopedClusterClient.mockClear(); - await root.shutdown(); - }); - - it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => { - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, res, toolkit) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return toolkit.authenticated({ state: user }); - } else { - return res.unauthorized(); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: () => 'ok from legacy server', - }); - - const response = await kbnTestServer.request - .get(root, legacyUrl) - .expect(200, 'ok from legacy server'); - - expect(response.header['set-cookie']).toHaveLength(1); - }); - - it('passes authHeaders as request headers to the legacy platform', async () => { - const token = 'Basic: name:password'; - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, res, toolkit) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return toolkit.authenticated({ - state: user, - requestHeaders: { - authorization: token, - }, - }); - } else { - return res.unauthorized(); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: (req: Request) => ({ - authorization: req.headers.authorization, - custom: req.headers.custom, - }), - }); - - await kbnTestServer.request - .get(root, legacyUrl) - .set({ custom: 'custom-header' }) - .expect(200, { authorization: token, custom: 'custom-header' }); - }); - - it('attach security header to a successful response handled by Legacy platform', async () => { - const authResponseHeader = { - 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', - }; - const { http } = await root.setup(); - const { registerAuth } = http; - - registerAuth((req, res, toolkit) => { - return toolkit.authenticated({ responseHeaders: authResponseHeader }); - }); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: '/legacy', - handler: () => 'ok', - }); - - const response = await kbnTestServer.request.get(root, '/legacy').expect(200); - expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); - }); - - it('attach security header to an error response handled by Legacy platform', async () => { - const authResponseHeader = { - 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', - }; - const { http } = await root.setup(); - const { registerAuth } = http; - - registerAuth((req, res, toolkit) => { - return toolkit.authenticated({ responseHeaders: authResponseHeader }); - }); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: '/legacy', - handler: () => { - throw Boom.badRequest(); - }, - }); - - const response = await kbnTestServer.request.get(root, '/legacy').expect(400); - expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); - }); - }); - - describe('#basePath()', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - }, 30000); - - afterEach(async () => await root.shutdown()); - it('basePath information for an incoming request is available in legacy server', async () => { - const reqBasePath = '/requests-specific-base-path'; - const { http } = await root.setup(); - http.registerOnPreRouting((req, res, toolkit) => { - http.basePath.set(req, reqBasePath); - return toolkit.next(); - }); - - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: kbnServer.newPlatform.setup.core.http.basePath.get, - }); - - await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath); - }); - }); - }); describe('legacy elasticsearch client', () => { let root: ReturnType; beforeEach(async () => { diff --git a/src/core/server/i18n/get_kibana_translation_files.test.ts b/src/core/server/i18n/get_kibana_translation_files.test.ts index 7ca0fe0e79337..45e1a8dfec9cb 100644 --- a/src/core/server/i18n/get_kibana_translation_files.test.ts +++ b/src/core/server/i18n/get_kibana_translation_files.test.ts @@ -14,7 +14,7 @@ const mockGetTranslationPaths = getTranslationPaths as jest.Mock; jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn().mockResolvedValue([]), })); -jest.mock('../utils', () => ({ +jest.mock('@kbn/utils', () => ({ fromRoot: jest.fn().mockImplementation((path: string) => path), })); diff --git a/src/core/server/i18n/get_kibana_translation_files.ts b/src/core/server/i18n/get_kibana_translation_files.ts index 7b5ada2a25f4f..4e7ee718113ce 100644 --- a/src/core/server/i18n/get_kibana_translation_files.ts +++ b/src/core/server/i18n/get_kibana_translation_files.ts @@ -7,7 +7,7 @@ */ import { basename } from 'path'; -import { fromRoot } from '../utils'; +import { fromRoot } from '@kbn/utils'; import { getTranslationPaths } from './get_translation_paths'; export const getKibanaTranslationFiles = async ( diff --git a/src/core/server/i18n/get_translation_paths.test.ts b/src/core/server/i18n/get_translation_paths.test.ts index 9094b008be739..3e9d68c16d30e 100644 --- a/src/core/server/i18n/get_translation_paths.test.ts +++ b/src/core/server/i18n/get_translation_paths.test.ts @@ -23,14 +23,14 @@ describe('getTranslationPaths', () => { getTranslationPaths({ cwd: '/some/cwd', nested: false }); expect(globbyMock).toHaveBeenCalledTimes(1); - expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd' }); + expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd', dot: true }); globbyMock.mockClear(); await getTranslationPaths({ cwd: '/other/cwd', nested: true }); expect(globbyMock).toHaveBeenCalledTimes(1); - expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd' }); + expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd', dot: true }); }); it('calls `readFile` for each entry returned by `globby`', async () => { diff --git a/src/core/server/i18n/get_translation_paths.ts b/src/core/server/i18n/get_translation_paths.ts index 93b10da73dcc7..8897786252d40 100644 --- a/src/core/server/i18n/get_translation_paths.ts +++ b/src/core/server/i18n/get_translation_paths.ts @@ -18,7 +18,7 @@ const I18N_RC = '.i18nrc.json'; export async function getTranslationPaths({ cwd, nested }: { cwd: string; nested: boolean }) { const glob = nested ? `*/${I18N_RC}` : I18N_RC; - const entries = await globby(glob, { cwd }); + const entries = await globby(glob, { cwd, dot: true }); const translationPaths: string[] = []; for (const entry of entries) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 963b69eac4f7f..2c6fa74cb54a0 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -406,8 +406,6 @@ export type { SavedObjectsMigrationVersion, } from './types'; -export type { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig } from './legacy'; - export { ServiceStatusLevels } from './status'; export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status'; diff --git a/src/core/server/kibana_config.test.ts b/src/core/server/kibana_config.test.ts index 1acdff9dd78e6..47bb6cf2a064c 100644 --- a/src/core/server/kibana_config.test.ts +++ b/src/core/server/kibana_config.test.ts @@ -7,28 +7,16 @@ */ import { config } from './kibana_config'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; +import { getDeprecationsFor } from './config/test_utils'; const CONFIG_PATH = 'kibana'; -const applyKibanaDeprecations = (settings: Record = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config: any = {}; - _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyKibanaDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: config.deprecations!, + settings, + path: CONFIG_PATH, + }); it('set correct defaults ', () => { const configValue = config.schema.validate({}); diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap deleted file mode 100644 index 69b7f9fc78315..0000000000000 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` -Array [ - Array [ - Object { - "logging": Object { - "verbose": true, - }, - "path": Object {}, - }, - ], -] -`; - -exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` -Array [ - Array [ - Object { - "logging": Object { - "verbose": true, - }, - "path": Object {}, - }, - ], -] -`; diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts deleted file mode 100644 index febf91625378d..0000000000000 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ensureValidConfiguration } from './ensure_valid_configuration'; -import { getUnusedConfigKeys } from './get_unused_config_keys'; -import { configServiceMock } from '../../config/mocks'; - -jest.mock('./get_unused_config_keys'); - -describe('ensureValidConfiguration', () => { - let configService: ReturnType; - - beforeEach(() => { - jest.clearAllMocks(); - configService = configServiceMock.create(); - configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic'])); - - (getUnusedConfigKeys as any).mockImplementation(() => []); - }); - - it('calls getUnusedConfigKeys with correct parameters', async () => { - await ensureValidConfiguration( - configService as any, - { - settings: 'settings', - legacyConfig: 'pluginExtendedConfig', - } as any - ); - expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); - expect(getUnusedConfigKeys).toHaveBeenCalledWith({ - coreHandledConfigPaths: ['core', 'elastic'], - settings: 'settings', - legacyConfig: 'pluginExtendedConfig', - }); - }); - - it('returns normally when there is no unused keys', async () => { - await expect( - ensureValidConfiguration(configService as any, {} as any) - ).resolves.toBeUndefined(); - - expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); - }); - - it('throws when there are some unused keys', async () => { - (getUnusedConfigKeys as any).mockImplementation(() => ['some.key', 'some.other.key']); - - await expect( - ensureValidConfiguration(configService as any, {} as any) - ).rejects.toMatchInlineSnapshot( - `[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]` - ); - }); -}); diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts deleted file mode 100644 index 86b4e0aeeea59..0000000000000 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyConfig, LegacyVars } from '../types'; -import { getUnusedConfigKeys } from './get_unused_config_keys'; - -describe('getUnusedConfigKeys', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - const getConfig = (values: LegacyVars = {}): LegacyConfig => - ({ - get: () => values as any, - } as LegacyConfig); - - describe('not using core or plugin specs', () => { - it('should return an empty list for empty parameters', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: {}, - legacyConfig: getConfig(), - }) - ).toEqual([]); - }); - - it('returns empty list when config and settings have the same properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - alsoInBoth: 'someValue', - }, - legacyConfig: getConfig({ - presentInBoth: true, - alsoInBoth: 'someValue', - }), - }) - ).toEqual([]); - }); - - it('returns empty list when config has entries not present in settings', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - }, - legacyConfig: getConfig({ - presentInBoth: true, - onlyInConfig: 'someValue', - }), - }) - ).toEqual([]); - }); - - it('returns the list of properties from settings not present in config', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - onlyInSetting: 'value', - }, - legacyConfig: getConfig({ - presentInBoth: true, - }), - }) - ).toEqual(['onlyInSetting']); - }); - - it('correctly handle nested properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - elasticsearch: { - username: 'foo', - password: 'bar', - }, - }, - legacyConfig: getConfig({ - elasticsearch: { - username: 'foo', - onlyInConfig: 'default', - }, - }), - }) - ).toEqual(['elasticsearch.password']); - }); - - it('correctly handle "env" specific case', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - env: 'development', - }, - legacyConfig: getConfig({ - env: { - name: 'development', - }, - }), - }) - ).toEqual([]); - }); - - it('correctly handle array properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - prop: ['a', 'b', 'c'], - }, - legacyConfig: getConfig({ - prop: ['a'], - }), - }) - ).toEqual([]); - }); - }); - - it('ignores properties managed by the new platform', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: ['core', 'foo.bar'], - settings: { - core: { - prop: 'value', - }, - foo: { - bar: true, - dolly: true, - }, - }, - legacyConfig: getConfig({}), - }) - ).toEqual(['foo.dolly']); - }); - - it('handles array values', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: ['core', 'array'], - settings: { - core: { - prop: 'value', - array: [1, 2, 3], - }, - array: ['some', 'values'], - }, - legacyConfig: getConfig({}), - }) - ).toEqual([]); - }); -}); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts deleted file mode 100644 index a2da6dc97225e..0000000000000 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { difference } from 'lodash'; -import { getFlattenedObject } from '@kbn/std'; -import { hasConfigPathIntersection } from '../../config'; -import { LegacyConfig, LegacyVars } from '../types'; - -const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object)); - -export async function getUnusedConfigKeys({ - coreHandledConfigPaths, - settings, - legacyConfig, -}: { - coreHandledConfigPaths: string[]; - settings: LegacyVars; - legacyConfig: LegacyConfig; -}) { - const inputKeys = getFlattenedKeys(settings); - const appliedKeys = getFlattenedKeys(legacyConfig.get()); - - if (inputKeys.includes('env')) { - // env is a special case key, see https://github.com/elastic/kibana/blob/848bf17b/src/legacy/server/config/config.js#L74 - // where it is deleted from the settings before being injected into the schema via context and - // then renamed to `env.name` https://github.com/elastic/kibana/blob/848bf17/src/legacy/server/config/schema.js#L17 - inputKeys[inputKeys.indexOf('env')] = 'env.name'; - } - - // Filter out keys that are marked as used in the core (e.g. by new core plugins). - return difference(inputKeys, appliedKeys).filter( - (unusedConfigKey) => - !coreHandledConfigPaths.some((usedInCoreConfigKey) => - hasConfigPathIntersection(unusedConfigKey, usedInCoreConfigKey) - ) - ); -} diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts index 8614265e4375d..39ffef501a9ec 100644 --- a/src/core/server/legacy/index.ts +++ b/src/core/server/legacy/index.ts @@ -6,16 +6,6 @@ * Side Public License, v 1. */ -/** @internal */ -export { ensureValidConfiguration } from './config'; /** @internal */ export type { ILegacyService } from './legacy_service'; export { LegacyService } from './legacy_service'; -/** @internal */ -export type { - LegacyVars, - LegacyConfig, - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyServiceSetupConfig, -} from './types'; diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts deleted file mode 100644 index 715749c6ef0cb..0000000000000 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ /dev/null @@ -1,65 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as kbnTestServer from '../../../test_helpers/kbn_server'; - -describe('legacy service', () => { - describe('http server', () => { - let root: ReturnType; - beforeEach(() => { - root = kbnTestServer.createRoot({ - migrations: { skip: true }, - plugins: { initialize: false }, - }); - }, 30000); - - afterEach(async () => await root.shutdown()); - - it("handles http request in Legacy platform if New platform doesn't handle it", async () => { - const { http } = await root.setup(); - const rootUrl = '/route'; - const router = http.createRouter(rootUrl); - router.get({ path: '/new-platform', validate: false }, (context, req, res) => - res.ok({ body: 'from-new-platform' }) - ); - - await root.start(); - - const legacyPlatformUrl = `${rootUrl}/legacy-platform`; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyPlatformUrl, - handler: () => 'ok from legacy server', - }); - - await kbnTestServer.request.get(root, '/route/new-platform').expect(200, 'from-new-platform'); - - await kbnTestServer.request.get(root, legacyPlatformUrl).expect(200, 'ok from legacy server'); - }); - it('throws error if Legacy and New platforms register handler for the same route', async () => { - const { http } = await root.setup(); - const rootUrl = '/route'; - const router = http.createRouter(rootUrl); - router.get({ path: '', validate: false }, (context, req, res) => - res.ok({ body: 'from-new-platform' }) - ); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - expect(() => - kbnServer.server.route({ - method: 'GET', - path: rootUrl, - handler: () => 'ok from legacy server', - }) - ).toThrowErrorMatchingInlineSnapshot(`"New route /route conflicts with existing /route"`); - }); - }); -}); diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index 1f4c308be0107..0d72318a630e0 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -8,26 +8,14 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { LegacyService } from './legacy_service'; -import { LegacyConfig, LegacyServiceSetupDeps } from './types'; -type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; +type LegacyServiceMock = jest.Mocked>; const createLegacyServiceMock = (): LegacyServiceMock => ({ - legacyId: Symbol(), - setupLegacyConfig: jest.fn(), setup: jest.fn(), - start: jest.fn(), stop: jest.fn(), }); -const createLegacyConfigMock = (): jest.Mocked => ({ - get: jest.fn(), - has: jest.fn(), - set: jest.fn(), -}); - export const legacyServiceMock = { create: createLegacyServiceMock, - createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), - createLegacyConfig: createLegacyConfigMock, }; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts new file mode 100644 index 0000000000000..506f0fd6f96d3 --- /dev/null +++ b/src/core/server/legacy/legacy_service.test.mocks.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const reconfigureLoggingMock = jest.fn(); +export const setupLoggingMock = jest.fn(); +export const setupLoggingRotateMock = jest.fn(); + +jest.doMock('@kbn/legacy-logging', () => ({ + ...(jest.requireActual('@kbn/legacy-logging') as any), + reconfigureLogging: reconfigureLoggingMock, + setupLogging: setupLoggingMock, + setupLoggingRotate: setupLoggingRotateMock, +})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d0a02b9859960..6b20bd7434baf 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -6,35 +6,22 @@ * Side Public License, v 1. */ -jest.mock('../../../legacy/server/kbn_server'); - -import { BehaviorSubject, throwError } from 'rxjs'; +import { + setupLoggingMock, + setupLoggingRotateMock, + reconfigureLoggingMock, +} from './legacy_service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import moment from 'moment'; import { REPO_ROOT } from '@kbn/dev-utils'; -import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; -import { DiscoveredPlugin } from '../plugins'; import { getEnvOptions, configServiceMock } from '../config/mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; -import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; -import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; -import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; -import { environmentServiceMock } from '../environment/environment_service.mock'; -import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; -import { LegacyService } from './legacy_service'; -import { coreMock } from '../mocks'; -import { statusServiceMock } from '../status/status_service.mock'; -import { loggingServiceMock } from '../logging/logging_service.mock'; -import { metricsServiceMock } from '../metrics/metrics_service.mock'; -import { i18nServiceMock } from '../i18n/i18n_service.mock'; -import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; - -const MockKbnServer: jest.Mock = KbnServer as any; +import { LegacyService, LegacyServiceSetupDeps } from './legacy_service'; let coreId: symbol; let env: Env; @@ -42,70 +29,16 @@ let config$: BehaviorSubject; let setupDeps: LegacyServiceSetupDeps; -let startDeps: LegacyServiceStartDeps; - const logger = loggingSystemMock.create(); let configService: ReturnType; -let environmentSetup: ReturnType; beforeEach(() => { coreId = Symbol(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); configService = configServiceMock.create(); - environmentSetup = environmentServiceMock.createSetupContract(); - - MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); - MockKbnServer.prototype.listen = jest.fn(); setupDeps = { - core: { - capabilities: capabilitiesServiceMock.createSetupContract(), - context: contextServiceMock.createSetupContract(), - elasticsearch: { legacy: {} } as any, - i18n: i18nServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), - http: { - ...httpServiceMock.createInternalSetupContract(), - auth: { - getAuthHeaders: () => undefined, - } as any, - }, - httpResources: httpResourcesMock.createSetupContract(), - savedObjects: savedObjectsServiceMock.createInternalSetupContract(), - plugins: { - initialized: true, - contracts: new Map([['plugin-id', 'plugin-value']]), - }, - rendering: renderingServiceMock, - environment: environmentSetup, - status: statusServiceMock.createInternalSetupContract(), - logging: loggingServiceMock.createInternalSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), - deprecations: deprecationsServiceMock.createInternalSetupContract(), - }, - plugins: { 'plugin-id': 'plugin-value' }, - uiPlugins: { - public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([ - [ - 'plugin-id', - { - requiredBundles: [], - publicTargetDir: 'path/to/target/public', - publicAssetsDir: '/plugins/name/assets/', - }, - ], - ]), - browserConfigs: new Map(), - }, - }; - - startDeps = { - core: { - ...coreMock.createInternalStart(), - plugins: { contracts: new Map() }, - }, - plugins: {}, + http: httpServiceMock.createInternalSetupContract(), }; config$ = new BehaviorSubject( @@ -116,98 +49,78 @@ beforeEach(() => { ); configService.getConfig$.mockReturnValue(config$); - configService.getUsedPaths.mockResolvedValue(['foo.bar']); }); afterEach(() => { jest.clearAllMocks(); + setupLoggingMock.mockReset(); + setupLoggingRotateMock.mockReset(); + reconfigureLoggingMock.mockReset(); }); -describe('once LegacyService is set up with connection info', () => { - test('creates legacy kbnServer and calls `listen`.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService, +describe('#setup', () => { + it('initializes legacy logging', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) - ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual( - expect.objectContaining({ - path: expect.objectContaining({ autoListen: true }), - server: expect.objectContaining({ autoListen: true }), - }) - ); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); - expect(mockKbnServer.close).not.toHaveBeenCalled(); - }); - - test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); - const legacyService = new LegacyService({ coreId, env, logger, configService: configService as any, }); - await legacyService.setupLegacyConfig(); + await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: false }, server: { autoListen: true } }, - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) + expect(setupLoggingMock).toHaveBeenCalledTimes(1); + expect(setupLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - const legacyConfig = MockKbnServer.mock.calls[0][1].get(); - expect(legacyConfig.path.autoListen).toBe(false); - expect(legacyConfig.server.autoListen).toBe(true); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); - expect(mockKbnServer.listen).not.toHaveBeenCalled(); - expect(mockKbnServer.close).not.toHaveBeenCalled(); + expect(setupLoggingRotateMock).toHaveBeenCalledTimes(1); + expect(setupLoggingRotateMock).toHaveBeenCalledWith(setupDeps.http.server, loggingConfig); }); - test('creates legacy kbnServer and closes it if `listen` fails.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + it('reloads the logging config when the config changes', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"something failed"` - ); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.listen).toHaveBeenCalled(); - expect(mockKbnServer.close).toHaveBeenCalled(); - }); - - test('throws if fails to retrieve initial config.', async () => { - configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); const legacyService = new LegacyService({ coreId, env, @@ -215,150 +128,70 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await expect(legacyService.setupLegacyConfig()).rejects.toThrowErrorMatchingInlineSnapshot( - `"something failed"` - ); - await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()"` - ); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); - - expect(MockKbnServer).not.toHaveBeenCalled(); - }); - - test('reconfigures logging configuration if new config is received.', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); - expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( - `applyLoggingConfiguration params` + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - }); - test('logs error if re-configuring fails.', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + loggingConfig$.next({ + foo: 'changed', }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([]); + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(2); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + { foo: 'changed' }, + opsConfig.interval.asMilliseconds() + ); + }); - const configError = new Error('something went wrong'); - mockKbnServer.applyLoggingConfiguration.mockImplementation(() => { - throw configError; + it('stops reloading logging config once the service is stopped', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); - - expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]); - }); - - test('logs error if config service fails.', async () => { const legacyService = new LegacyService({ coreId, env, logger, configService: configService as any, }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([]); - - const configError = new Error('something went wrong'); - config$.error(configError); - - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]); - }); -}); -describe('once LegacyService is set up without connection info', () => { - let legacyService: LegacyService; - beforeEach(async () => { - legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); - await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - }); - test('creates legacy kbnServer with `autoListen: false`.', () => { - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: {}, server: { autoListen: true } }, - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) - ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual( - expect.objectContaining({ - server: expect.objectContaining({ autoListen: true }), - }) + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - }); - - test('reconfigures logging configuration if new config is received.', async () => { - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + await legacyService.stop(); - expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( - `applyLoggingConfiguration params` - ); - }); -}); - -describe('start', () => { - test('Cannot start without setup phase', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + loggingConfig$.next({ + foo: 'changed', }); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); - }); -}); -test('Sets the server.uuid property on the legacy configuration', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); }); - - environmentSetup.instanceUuid = 'UUID_FROM_SERVICE'; - - const { legacyConfig } = await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - - expect(legacyConfig.get('server.uuid')).toBe('UUID_FROM_SERVICE'); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 43b348a5ff4a2..1d5343ff5311d 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -6,141 +6,61 @@ * Side Public License, v 1. */ -import { combineLatest, ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { first, map, publishReplay, tap } from 'rxjs/operators'; +import { combineLatest, Observable, Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { Server } from '@hapi/hapi'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PathConfigType } from '@kbn/utils'; +import { + reconfigureLogging, + setupLogging, + setupLoggingRotate, + LegacyLoggingConfig, +} from '@kbn/legacy-logging'; -import type { RequestHandlerContext } from 'src/core/server'; -// @ts-expect-error legacy config class -import { Config as LegacyConfigClass } from '../../../legacy/server/config'; -import { CoreService } from '../../types'; -import { Config } from '../config'; import { CoreContext } from '../core_context'; -import { CspConfigType, config as cspConfig } from '../csp'; -import { - HttpConfig, - HttpConfigType, - config as httpConfig, - IRouter, - RequestHandlerContextProvider, -} from '../http'; +import { config as loggingConfig } from '../logging'; +import { opsConfig, OpsConfigType } from '../metrics'; import { Logger } from '../logging'; -import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; -import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url'; -import { CoreSetup, CoreStart } from '..'; - -interface LegacyKbnServer { - applyLoggingConfiguration: (settings: Readonly) => void; - listen: () => Promise; - ready: () => Promise; - close: () => Promise; -} +import { InternalHttpServiceSetup } from '../http'; -function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) { - const rawConfig = config.toRaw(); - - // Elasticsearch config is solely handled by the core and legacy platform - // shouldn't have direct access to it. - if (rawConfig.elasticsearch !== undefined) { - delete rawConfig.elasticsearch; - } - - return { - ...rawConfig, - // We rely heavily in the default value of 'path.data' in the legacy world and, - // since it has been moved to NP, it won't show up in RawConfig. - path: pathConfig, - }; +export interface LegacyServiceSetupDeps { + http: InternalHttpServiceSetup; } /** @internal */ export type ILegacyService = PublicMethodsOf; /** @internal */ -export class LegacyService implements CoreService { - /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ - public readonly legacyId = Symbol(); +export class LegacyService { private readonly log: Logger; - private readonly httpConfig$: Observable; - private kbnServer?: LegacyKbnServer; + private readonly opsConfig$: Observable; + private readonly legacyLoggingConfig$: Observable; private configSubscription?: Subscription; - private setupDeps?: LegacyServiceSetupDeps; - private update$?: ConnectableObservable<[Config, PathConfigType]>; - private legacyRawConfig?: LegacyConfig; - private settings?: LegacyVars; - constructor(private readonly coreContext: CoreContext) { + constructor(coreContext: CoreContext) { const { logger, configService } = coreContext; this.log = logger.get('legacy-service'); - this.httpConfig$ = combineLatest( - configService.atPath(httpConfig.path), - configService.atPath(cspConfig.path), - configService.atPath(externalUrlConfig.path) - ).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); - } - - public async setupLegacyConfig() { - this.update$ = combineLatest([ - this.coreContext.configService.getConfig$(), - this.coreContext.configService.atPath('path'), - ]).pipe( - tap(([config, pathConfig]) => { - if (this.kbnServer !== undefined) { - this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig)); - } - }), - tap({ error: (err) => this.log.error(err) }), - publishReplay(1) - ) as ConnectableObservable<[Config, PathConfigType]>; - - this.configSubscription = this.update$.connect(); - - this.settings = await this.update$ - .pipe( - first(), - map(([config, pathConfig]) => getLegacyRawConfig(config, pathConfig)) - ) - .toPromise(); - - this.legacyRawConfig = LegacyConfigClass.withDefaultSchema(this.settings); - - return { - settings: this.settings, - legacyConfig: this.legacyRawConfig!, - }; + this.legacyLoggingConfig$ = configService.atPath(loggingConfig.path); + this.opsConfig$ = configService.atPath(opsConfig.path); } public async setup(setupDeps: LegacyServiceSetupDeps) { this.log.debug('setting up legacy service'); - - if (!this.legacyRawConfig) { - throw new Error( - 'Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()' - ); - } - - // propagate the instance uuid to the legacy config, as it was the legacy way to access it. - this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid); - - this.setupDeps = setupDeps; + await this.setupLegacyLogging(setupDeps.http.server); } - public async start(startDeps: LegacyServiceStartDeps) { - const { setupDeps } = this; - - if (!setupDeps || !this.legacyRawConfig) { - throw new Error('Legacy service is not setup yet.'); - } + private async setupLegacyLogging(server: Server) { + const legacyLoggingConfig = await this.legacyLoggingConfig$.pipe(first()).toPromise(); + const currentOpsConfig = await this.opsConfig$.pipe(first()).toPromise(); - this.log.debug('starting legacy service'); + await setupLogging(server, legacyLoggingConfig, currentOpsConfig.interval.asMilliseconds()); + await setupLoggingRotate(server, legacyLoggingConfig); - this.kbnServer = await this.createKbnServer( - this.settings!, - this.legacyRawConfig!, - setupDeps, - startDeps + this.configSubscription = combineLatest([this.legacyLoggingConfig$, this.opsConfig$]).subscribe( + ([newLoggingConfig, newOpsConfig]) => { + reconfigureLogging(server, newLoggingConfig, newOpsConfig.interval.asMilliseconds()); + } ); } @@ -151,156 +71,5 @@ export class LegacyService implements CoreService { this.configSubscription.unsubscribe(); this.configSubscription = undefined; } - - if (this.kbnServer !== undefined) { - await this.kbnServer.close(); - this.kbnServer = undefined; - } - } - - private async createKbnServer( - settings: LegacyVars, - config: LegacyConfig, - setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps - ) { - const coreStart: CoreStart = { - capabilities: startDeps.core.capabilities, - elasticsearch: startDeps.core.elasticsearch, - http: { - auth: startDeps.core.http.auth, - basePath: startDeps.core.http.basePath, - getServerInfo: startDeps.core.http.getServerInfo, - }, - savedObjects: { - getScopedClient: startDeps.core.savedObjects.getScopedClient, - createScopedRepository: startDeps.core.savedObjects.createScopedRepository, - createInternalRepository: startDeps.core.savedObjects.createInternalRepository, - createSerializer: startDeps.core.savedObjects.createSerializer, - createExporter: startDeps.core.savedObjects.createExporter, - createImporter: startDeps.core.savedObjects.createImporter, - getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, - }, - metrics: { - collectionInterval: startDeps.core.metrics.collectionInterval, - getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, - }, - uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, - coreUsageData: { - getCoreUsageData: () => { - throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy'); - }, - }, - }; - - const router = setupDeps.core.http.createRouter('', this.legacyId); - const coreSetup: CoreSetup = { - capabilities: setupDeps.core.capabilities, - context: setupDeps.core.context, - elasticsearch: { - legacy: setupDeps.core.elasticsearch.legacy, - }, - http: { - createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory, - registerRouteHandlerContext: < - Context extends RequestHandlerContext, - ContextName extends keyof Context - >( - contextName: ContextName, - provider: RequestHandlerContextProvider - ) => setupDeps.core.http.registerRouteHandlerContext(this.legacyId, contextName, provider), - createRouter: () => - router as IRouter, - resources: setupDeps.core.httpResources.createRegistrar(router), - registerOnPreRouting: setupDeps.core.http.registerOnPreRouting, - registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, - registerAuth: setupDeps.core.http.registerAuth, - registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, - registerOnPreResponse: setupDeps.core.http.registerOnPreResponse, - basePath: setupDeps.core.http.basePath, - auth: { - get: setupDeps.core.http.auth.get, - isAuthenticated: setupDeps.core.http.auth.isAuthenticated, - }, - csp: setupDeps.core.http.csp, - getServerInfo: setupDeps.core.http.getServerInfo, - }, - i18n: setupDeps.core.i18n, - logging: { - configure: (config$) => setupDeps.core.logging.configure([], config$), - }, - metrics: { - collectionInterval: setupDeps.core.metrics.collectionInterval, - getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, - }, - savedObjects: { - setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, - addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, - registerType: setupDeps.core.savedObjects.registerType, - }, - status: { - isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, - core$: setupDeps.core.status.core$, - overall$: setupDeps.core.status.overall$, - set: () => { - throw new Error(`core.status.set is unsupported in legacy`); - }, - // @ts-expect-error - get dependencies$() { - throw new Error(`core.status.dependencies$ is unsupported in legacy`); - }, - // @ts-expect-error - get derivedStatus$() { - throw new Error(`core.status.derivedStatus$ is unsupported in legacy`); - }, - }, - uiSettings: { - register: setupDeps.core.uiSettings.register, - }, - deprecations: { - registerDeprecations: () => { - throw new Error('core.setup.deprecations.registerDeprecations is unsupported in legacy'); - }, - }, - getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), - }; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer(settings, config, { - env: { - mode: this.coreContext.env.mode, - packageInfo: this.coreContext.env.packageInfo, - }, - setupDeps: { - core: coreSetup, - plugins: setupDeps.plugins, - }, - startDeps: { - core: coreStart, - plugins: startDeps.plugins, - }, - __internals: { - hapiServer: setupDeps.core.http.server, - uiPlugins: setupDeps.uiPlugins, - rendering: setupDeps.core.rendering, - }, - logger: this.coreContext.logger, - }); - - const { autoListen } = await this.httpConfig$.pipe(first()).toPromise(); - - if (autoListen) { - try { - await kbnServer.listen(); - } catch (err) { - await kbnServer.close(); - throw err; - } - } else { - await kbnServer.ready(); - } - - return kbnServer; } } diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index a89441a5671b5..7e02d00c7b234 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -9,11 +9,10 @@ import { schema } from '@kbn/config-schema'; import { LegacyLoggingServer } from '@kbn/legacy-logging'; import { DisposableAppender, LogRecord } from '@kbn/logging'; -import { LegacyVars } from '../../types'; export interface LegacyAppenderConfig { type: 'legacy-appender'; - legacyLoggingConfig?: any; + legacyLoggingConfig?: Record; } /** @@ -23,7 +22,7 @@ export interface LegacyAppenderConfig { export class LegacyAppender implements DisposableAppender { public static configSchema = schema.object({ type: schema.literal('legacy-appender'), - legacyLoggingConfig: schema.any(), + legacyLoggingConfig: schema.recordOf(schema.string(), schema.any()), }); /** @@ -34,7 +33,7 @@ export class LegacyAppender implements DisposableAppender { private readonly loggingServer: LegacyLoggingServer; - constructor(legacyLoggingConfig: Readonly) { + constructor(legacyLoggingConfig: any) { this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig); } diff --git a/src/core/server/legacy/merge_vars.test.ts b/src/core/server/legacy/merge_vars.test.ts deleted file mode 100644 index e4268a52aa8ca..0000000000000 --- a/src/core/server/legacy/merge_vars.test.ts +++ /dev/null @@ -1,188 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { mergeVars } from './merge_vars'; - -describe('mergeVars', () => { - it('merges two objects together', () => { - const first = { - otherName: 'value', - otherCanFoo: true, - otherNested: { - otherAnotherVariable: 'ok', - }, - }; - const second = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - otherName: 'value', - otherCanFoo: true, - otherNested: { - otherAnotherVariable: 'ok', - }, - }); - }); - - it('does not mutate the source objects', () => { - const first = { - var1: 'first', - }; - const second = { - var1: 'second', - var2: 'second', - }; - const third = { - var1: 'third', - var2: 'third', - var3: 'third', - }; - const fourth = { - var1: 'fourth', - var2: 'fourth', - var3: 'fourth', - var4: 'fourth', - }; - - mergeVars(first, second, third, fourth); - - expect(first).toEqual({ var1: 'first' }); - expect(second).toEqual({ var1: 'second', var2: 'second' }); - expect(third).toEqual({ var1: 'third', var2: 'third', var3: 'third' }); - expect(fourth).toEqual({ var1: 'fourth', var2: 'fourth', var3: 'fourth', var4: 'fourth' }); - }); - - it('merges multiple objects together with precedence increasing from left-to-right', () => { - const first = { - var1: 'first', - var2: 'first', - var3: 'first', - var4: 'first', - }; - const second = { - var1: 'second', - var2: 'second', - var3: 'second', - }; - const third = { - var1: 'third', - var2: 'third', - }; - const fourth = { - var1: 'fourth', - }; - - expect(mergeVars(first, second, third, fourth)).toEqual({ - var1: 'fourth', - var2: 'third', - var3: 'second', - var4: 'first', - }); - }); - - it('overwrites the original variable value if a duplicate entry is found', () => { - const first = { - nested: { - otherAnotherVariable: 'ok', - }, - }; - const second = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }); - }); - - it('combines entries within "uiCapabilities"', () => { - const first = { - uiCapabilities: { - firstCapability: 'ok', - sharedCapability: 'shared', - }, - }; - const second = { - name: 'value', - canFoo: true, - uiCapabilities: { - secondCapability: 'ok', - }, - }; - const third = { - name: 'value', - canFoo: true, - uiCapabilities: { - thirdCapability: 'ok', - sharedCapability: 'blocked', - }, - }; - - expect(mergeVars(first, second, third)).toEqual({ - name: 'value', - canFoo: true, - uiCapabilities: { - firstCapability: 'ok', - secondCapability: 'ok', - thirdCapability: 'ok', - sharedCapability: 'blocked', - }, - }); - }); - - it('does not deeply combine entries within "uiCapabilities"', () => { - const first = { - uiCapabilities: { - firstCapability: 'ok', - nestedCapability: { - otherNestedProp: 'otherNestedValue', - }, - }, - }; - const second = { - name: 'value', - canFoo: true, - uiCapabilities: { - secondCapability: 'ok', - nestedCapability: { - nestedProp: 'nestedValue', - }, - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - uiCapabilities: { - firstCapability: 'ok', - secondCapability: 'ok', - nestedCapability: { - nestedProp: 'nestedValue', - }, - }, - }); - }); -}); diff --git a/src/core/server/legacy/merge_vars.ts b/src/core/server/legacy/merge_vars.ts deleted file mode 100644 index cd2cbb0d8cde2..0000000000000 --- a/src/core/server/legacy/merge_vars.ts +++ /dev/null @@ -1,23 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyVars } from './types'; - -const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities']; - -export function mergeVars(...sources: LegacyVars[]): LegacyVars { - return Object.assign( - {}, - ...sources, - ...ELIGIBLE_FLAT_MERGE_KEYS.flatMap((key) => - sources.some((source) => key in source) - ? [{ [key]: Object.assign({}, ...sources.map((source) => source[key] || {})) }] - : [] - ) - ); -} diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts deleted file mode 100644 index 9f562d3da3029..0000000000000 --- a/src/core/server/legacy/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; -import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; -import { InternalRenderingServiceSetup } from '../rendering'; - -/** - * @internal - * @deprecated - */ -export type LegacyVars = Record; - -type LegacyCoreSetup = InternalCoreSetup & { - plugins: PluginsServiceSetup; - rendering: InternalRenderingServiceSetup; -}; -type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart }; - -/** - * New platform representation of the legacy configuration (KibanaConfig) - * - * @internal - * @deprecated - */ -export interface LegacyConfig { - get(key?: string): T; - has(key: string): boolean; - set(key: string, value: any): void; - set(config: LegacyVars): void; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceSetupDeps { - core: LegacyCoreSetup; - plugins: Record; - uiPlugins: UiPlugins; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceStartDeps { - core: LegacyCoreStart; - plugins: Record; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyServiceSetupConfig { - legacyConfig: LegacyConfig; - settings: LegacyVars; -} diff --git a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap deleted file mode 100644 index fe1407563a635..0000000000000 --- a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`\`schema\` creates correct schema with defaults. 1`] = ` -Object { - "appenders": Map {}, - "loggers": Array [], - "root": Object { - "appenders": Array [ - "default", - ], - "level": "info", - }, -} -`; - -exports[`\`schema\` throws if \`root\` logger does not have "default" appender configured. 1`] = `"[root]: \\"default\\" appender required for migration period till the next major release"`; - -exports[`\`schema\` throws if \`root\` logger does not have appenders configured. 1`] = `"[root.appenders]: array size is [0], but cannot be smaller than [1]"`; - -exports[`fails if loggers use unknown appenders. 1`] = `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`; diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index 83f3c139e371a..e0004ba992c17 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -9,7 +9,35 @@ import { LoggingConfig, config } from './logging_config'; test('`schema` creates correct schema with defaults.', () => { - expect(config.schema.validate({})).toMatchSnapshot(); + expect(config.schema.validate({})).toMatchInlineSnapshot( + { json: expect.any(Boolean) }, // default value depends on TTY + ` + Object { + "appenders": Map {}, + "dest": "stdout", + "events": Object {}, + "filter": Object {}, + "json": Any, + "loggers": Array [], + "quiet": false, + "root": Object { + "appenders": Array [ + "default", + ], + "level": "info", + }, + "rotate": Object { + "enabled": false, + "everyBytes": 10485760, + "keepFiles": 7, + "pollingInterval": 10000, + "usePolling": false, + }, + "silent": false, + "verbose": false, + } + ` + ); }); test('`schema` throws if `root` logger does not have appenders configured.', () => { @@ -19,7 +47,9 @@ test('`schema` throws if `root` logger does not have appenders configured.', () appenders: [], }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[root.appenders]: array size is [0], but cannot be smaller than [1]"` + ); }); test('`schema` throws if `root` logger does not have "default" appender configured.', () => { @@ -29,7 +59,9 @@ test('`schema` throws if `root` logger does not have "default" appender configur appenders: ['console'], }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[root]: \\"default\\" appender required for migration period till the next major release"` + ); }); test('`getParentLoggerContext()` returns correct parent context name.', () => { @@ -157,7 +189,9 @@ test('fails if loggers use unknown appenders.', () => { ], }); - expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingSnapshot(); + expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingInlineSnapshot( + `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."` + ); }); describe('extend', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 24496289fb4c8..f5b75d7bb739c 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -7,6 +7,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; import { AppenderConfigType, Appenders } from './appenders/appenders'; // We need this helper for the types to be correct @@ -59,7 +60,7 @@ export const loggerSchema = schema.object({ export type LoggerConfigType = TypeOf; export const config = { path: 'logging', - schema: schema.object({ + schema: legacyLoggingConfigSchema.extends({ appenders: schema.mapOf(schema.string(), Appenders.configSchema, { defaultValue: new Map(), }), @@ -85,7 +86,7 @@ export const config = { }), }; -export type LoggingConfigType = Omit, 'appenders'> & { +export type LoggingConfigType = Pick, 'loggers' | 'root'> & { appenders: Map; }; @@ -105,6 +106,7 @@ export const loggerContextConfigSchema = schema.object({ /** @public */ export type LoggerContextConfigType = TypeOf; + /** @public */ export interface LoggerContextConfigInput { // config-schema knows how to handle either Maps or Records diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index 8a6fe71bc6222..b67be384732cb 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -16,6 +16,7 @@ jest.mock('fs', () => ({ const dynamicProps = { process: { pid: expect.any(Number) } }; jest.mock('@kbn/legacy-logging', () => ({ + ...(jest.requireActual('@kbn/legacy-logging') as any), setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), })); diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts index 3e358edf3a01e..0631bb2b35801 100644 --- a/src/core/server/metrics/index.ts +++ b/src/core/server/metrics/index.ts @@ -16,3 +16,4 @@ export type { export type { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; export { MetricsService } from './metrics_service'; export { opsConfig } from './ops_config'; +export type { OpsConfigType } from './ops_config'; diff --git a/src/core/server/plugins/legacy_config.test.ts b/src/core/server/plugins/legacy_config.test.ts index 5687c2dd551d2..0ea26f2e0333e 100644 --- a/src/core/server/plugins/legacy_config.test.ts +++ b/src/core/server/plugins/legacy_config.test.ts @@ -13,7 +13,7 @@ import { getGlobalConfig, getGlobalConfig$ } from './legacy_config'; import { REPO_ROOT } from '@kbn/utils'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { duration } from 'moment'; -import { fromRoot } from '../utils'; +import { fromRoot } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { Server } from '../server'; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index b10bc47cb825b..e37d985d42321 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -9,6 +9,7 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { fromRoot } from '@kbn/utils'; import { createPluginInitializerContext, InstanceInfo } from './plugin_context'; import { CoreContext } from '../core_context'; import { Env } from '../config'; @@ -16,7 +17,6 @@ import { loggingSystemMock } from '../logging/logging_system.mock'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; import { PluginManifest } from './types'; import { Server } from '../server'; -import { fromRoot } from '../utils'; import { schema, ByteSizeValue } from '@kbn/config-schema'; import { ConfigService } from '@kbn/config'; diff --git a/src/core/server/plugins/plugins_config.ts b/src/core/server/plugins/plugins_config.ts index d565513ebb35b..45d80445f376e 100644 --- a/src/core/server/plugins/plugins_config.ts +++ b/src/core/server/plugins/plugins_config.ts @@ -7,20 +7,24 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { ServiceConfigDescriptor } from '../internal_types'; import { Env } from '../config'; -export type PluginsConfigType = TypeOf; +const configSchema = schema.object({ + initialize: schema.boolean({ defaultValue: true }), -export const config = { + /** + * Defines an array of directories where another plugin should be loaded from. + */ + paths: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export type PluginsConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { path: 'plugins', - schema: schema.object({ - initialize: schema.boolean({ defaultValue: true }), - - /** - * Defines an array of directories where another plugin should be loaded from. - */ - paths: schema.arrayOf(schema.string(), { defaultValue: [] }), - }), + schema: configSchema, + deprecations: ({ unusedFromRoot }) => [unusedFromRoot('plugins.scanDirs')], }; /** @internal */ diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 2d54648d22950..6bf7a1fadb4d3 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -562,12 +562,12 @@ describe('PluginsService', () => { plugin$: from([ createPlugin('plugin-1', { path: 'path-1', - version: 'some-version', + version: 'version-1', configPath: 'plugin1', }), createPlugin('plugin-2', { path: 'path-2', - version: 'some-version', + version: 'version-2', configPath: 'plugin2', }), ]), @@ -577,7 +577,7 @@ describe('PluginsService', () => { }); describe('uiPlugins.internal', () => { - it('includes disabled plugins', async () => { + it('contains internal properties for plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect(uiPlugins.internal).toMatchInlineSnapshot(` @@ -586,15 +586,23 @@ describe('PluginsService', () => { "publicAssetsDir": /path-1/public/assets, "publicTargetDir": /path-1/target/public, "requiredBundles": Array [], + "version": "version-1", }, "plugin-2" => Object { "publicAssetsDir": /path-2/public/assets, "publicTargetDir": /path-2/target/public, "requiredBundles": Array [], + "version": "version-2", }, } `); }); + + it('includes disabled plugins', async () => { + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); + expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']); + }); }); describe('plugin initialization', () => { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 8b33e2cf4cc6b..09be40ecaf2a2 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -222,6 +222,7 @@ export class PluginsService implements CoreService(); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a6086bd6f17e8..3a01049c5e1fe 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -224,12 +224,15 @@ export interface DiscoveredPlugin { */ export interface InternalPluginInfo { /** - * Bundles that must be loaded for this plugoin + * Version of the plugin + */ + readonly version: string; + /** + * Bundles that must be loaded for this plugin */ readonly requiredBundles: readonly string[]; /** - * Path to the target/public directory of the plugin which should be - * served + * Path to the target/public directory of the plugin which should be served */ readonly publicTargetDir: string; /** @@ -250,7 +253,9 @@ export interface Plugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; } @@ -267,7 +272,9 @@ export interface AsyncPlugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; } diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts index ea3843884df31..0abd8fd5a0057 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { getPluginsBundlePaths } from './get_plugin_bundle_paths'; const createUiPlugins = (pluginDeps: Record) => { @@ -16,12 +16,13 @@ const createUiPlugins = (pluginDeps: Record) => { browserConfigs: new Map(), }; - Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + const addPlugin = (pluginId: string, deps: string[]) => { uiPlugins.internal.set(pluginId, { requiredBundles: deps, + version: '8.0.0', publicTargetDir: '', publicAssetsDir: '', - } as any); + } as InternalPluginInfo); uiPlugins.public.set(pluginId, { id: pluginId, configPath: 'config-path', @@ -29,6 +30,12 @@ const createUiPlugins = (pluginDeps: Record) => { requiredPlugins: [], requiredBundles: deps, }); + + deps.forEach((dep) => addPlugin(dep, [])); + }; + + Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + addPlugin(pluginId, deps); }); return uiPlugins; @@ -56,13 +63,13 @@ describe('getPluginsBundlePaths', () => { }); expect(pluginBundlePaths.get('a')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/a/a.plugin.js', - publicPath: '/regular-bundle-path/plugin/a/', + bundlePath: '/regular-bundle-path/plugin/a/8.0.0/a.plugin.js', + publicPath: '/regular-bundle-path/plugin/a/8.0.0/', }); expect(pluginBundlePaths.get('b')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/b/b.plugin.js', - publicPath: '/regular-bundle-path/plugin/b/', + bundlePath: '/regular-bundle-path/plugin/b/8.0.0/b.plugin.js', + publicPath: '/regular-bundle-path/plugin/b/8.0.0/', }); }); }); diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts index c8291b2720a92..86ffdcf835f7b 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts @@ -25,9 +25,15 @@ export const getPluginsBundlePaths = ({ while (pluginsToProcess.length > 0) { const pluginId = pluginsToProcess.pop() as string; + const plugin = uiPlugins.internal.get(pluginId); + if (!plugin) { + continue; + } + const { version } = plugin; + pluginBundlePaths.set(pluginId, { - publicPath: `${regularBundlePath}/plugin/${pluginId}/`, - bundlePath: `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`, + publicPath: `${regularBundlePath}/plugin/${pluginId}/${version}/`, + bundlePath: `${regularBundlePath}/plugin/${pluginId}/${version}/${pluginId}.plugin.js`, }); const pluginBundleIds = uiPlugins.internal.get(pluginId)?.requiredBundles ?? []; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index a21078cbe1135..14ca73e7fcca0 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -163,7 +163,12 @@ describe('actions', () => { describe('searchForOutdatedDocuments', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.searchForOutdatedDocuments(client, 'new_index', { properties: {} }); + const task = Actions.searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'new_index', + outdatedDocumentsQuery: {}, + }); + try { await task(); } catch (e) { @@ -172,6 +177,29 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + + it('configures request according to given parameters', async () => { + const esClient = elasticsearchClientMock.createInternalClient(); + const query = {}; + const targetIndex = 'new_index'; + const batchSize = 1000; + const task = Actions.searchForOutdatedDocuments(esClient, { + batchSize, + targetIndex, + outdatedDocumentsQuery: query, + }); + + await task(); + + expect(esClient.search).toHaveBeenCalledTimes(1); + expect(esClient.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: targetIndex, + size: batchSize, + body: expect.objectContaining({ query }), + }) + ); + }); }); describe('bulkOverwriteTransformedDocuments', () => { diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 52fa99b724873..8ac683a29d657 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -9,11 +9,11 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; -import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; -import { pipe } from 'fp-ts/lib/pipeable'; +import type { estypes } from '@elastic/elasticsearch'; import { errors as EsErrors } from '@elastic/elasticsearch'; +import type { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { pipe } from 'fp-ts/lib/pipeable'; import { flow } from 'fp-ts/lib/function'; -import type { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; import { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; @@ -24,13 +24,10 @@ import { export type { RetryableEsClientError }; /** - * Batch size for updateByQuery, reindex & search operations. Smaller batches - * reduce the memory pressure on Elasticsearch and Kibana so are less likely - * to cause failures. - * TODO (profile/tune): How much smaller can we make this number before it - * starts impacting how long migrations take to perform? + * Batch size for updateByQuery and reindex operations. + * Uses the default value of 1000 for Elasticsearch reindex operation. */ -const BATCH_SIZE = 1000; +const BATCH_SIZE = 1_000; const DEFAULT_TIMEOUT = '60s'; /** Allocate 1 replica if there are enough data nodes, otherwise continue with 0 */ const INDEX_AUTO_EXPAND_REPLICAS = '0-1'; @@ -839,6 +836,12 @@ export interface SearchResponse { outdatedDocuments: SavedObjectsRawDoc[]; } +interface SearchForOutdatedDocumentsOptions { + batchSize: number; + targetIndex: string; + outdatedDocumentsQuery?: estypes.QueryContainer; +} + /** * Search for outdated saved object documents with the provided query. Will * return one batch of documents. Searching should be repeated until no more @@ -846,18 +849,17 @@ export interface SearchResponse { */ export const searchForOutdatedDocuments = ( client: ElasticsearchClient, - index: string, - query: Record + options: SearchForOutdatedDocumentsOptions ): TaskEither.TaskEither => () => { return client .search({ - index, + index: options.targetIndex, // Return the _seq_no and _primary_term so we can use optimistic // concurrency control for updates seq_no_primary_term: true, - size: BATCH_SIZE, + size: options.batchSize, body: { - query, + query: options.outdatedDocumentsQuery, // Optimize search performance by sorting by the "natural" index order sort: ['_doc'], }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 1824efa0ed8d4..aa9a5ea92ac11 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -59,7 +59,7 @@ describe('migration actions', () => { // Create test fixture data: await createIndex(client, 'existing_index_with_docs', { - dynamic: true as any, + dynamic: true, properties: {}, })(); const sourceDocs = ([ @@ -337,7 +337,6 @@ describe('migration actions', () => { // Reindex doesn't return any errors on it's own, so we have to test // together with waitForReindexTask describe('reindex & waitForReindexTask', () => { - expect.assertions(2); it('resolves right when reindex succeeds without reindex script', async () => { const res = (await reindex( client, @@ -354,11 +353,11 @@ describe('migration actions', () => { } `); - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1", @@ -384,11 +383,11 @@ describe('migration actions', () => { "right": "reindex_succeeded", } `); - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target_2', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_2', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1_updated", @@ -432,12 +431,12 @@ describe('migration actions', () => { } `); - // Assert that documents weren't overrided by the second, unscripted reindex - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target_3', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + // Assert that documents weren't overridden by the second, unscripted reindex + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_3', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1_updated", @@ -452,11 +451,11 @@ describe('migration actions', () => { // Simulate a reindex that only adds some of the documents from the // source index into the target index await createIndex(client, 'reindex_target_4', { properties: {} })(); - const sourceDocs = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - undefined as any - )()) as Either.Right).right.outdatedDocuments + const sourceDocs = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments .slice(0, 2) .map(({ _id, _source }) => ({ _id, @@ -479,13 +478,13 @@ describe('migration actions', () => { "right": "reindex_succeeded", } `); - // Assert that existing documents weren't overrided, but that missing + // Assert that existing documents weren't overridden, but that missing // documents were added by the reindex - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target_4', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_4', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1", @@ -701,26 +700,30 @@ describe('migration actions', () => { describe('searchForOutdatedDocuments', () => { it('only returns documents that match the outdatedDocumentsQuery', async () => { expect.assertions(2); - const resultsWithQuery = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - { + const resultsWithQuery = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: { match: { title: { query: 'doc' } }, - } - )()) as Either.Right).right.outdatedDocuments; + }, + })()) as Either.Right).right.outdatedDocuments; expect(resultsWithQuery.length).toBe(3); - const resultsWithoutQuery = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const resultsWithoutQuery = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(resultsWithoutQuery.length).toBe(4); }); it('resolves with _id, _source, _seq_no and _primary_term', async () => { expect.assertions(1); - const results = ((await searchForOutdatedDocuments(client, 'existing_index_with_docs', { - match: { title: { query: 'doc' } }, + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: { + match: { title: { query: 'doc' } }, + }, })()) as Either.Right).right.outdatedDocuments; expect(results).toEqual( expect.arrayContaining([ @@ -805,7 +808,7 @@ describe('migration actions', () => { it('resolves right when mappings were updated and picked up', async () => { // Create an index without any mappings and insert documents into it await createIndex(client, 'existing_index_without_mappings', { - dynamic: false as any, + dynamic: false, properties: {}, })(); const sourceDocs = ([ @@ -821,11 +824,13 @@ describe('migration actions', () => { )(); // Assert that we can't search over the unmapped fields of the document - const originalSearchResults = ((await searchForOutdatedDocuments( - client, - 'existing_index_without_mappings', - { match: { title: { query: 'doc' } } } - )()) as Either.Right).right.outdatedDocuments; + const originalSearchResults = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_without_mappings', + outdatedDocumentsQuery: { + match: { title: { query: 'doc' } }, + }, + })()) as Either.Right).right.outdatedDocuments; expect(originalSearchResults.length).toBe(0); // Update and pickup mappings so that the title field is searchable @@ -839,11 +844,13 @@ describe('migration actions', () => { await waitForPickupUpdatedMappingsTask(client, taskId, '60s')(); // Repeat the search expecting to be able to find the existing documents - const pickedUpSearchResults = ((await searchForOutdatedDocuments( - client, - 'existing_index_without_mappings', - { match: { title: { query: 'doc' } } } - )()) as Either.Right).right.outdatedDocuments; + const pickedUpSearchResults = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_without_mappings', + outdatedDocumentsQuery: { + match: { title: { query: 'doc' } }, + }, + })()) as Either.Right).right.outdatedDocuments; expect(pickedUpSearchResults.length).toBe(4); }); }); @@ -1050,11 +1057,11 @@ describe('migration actions', () => { `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { - const existingDocs = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const existingDocs = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; const task = bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', [ ...existingDocs, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index fd62fd107648e..4d41a147bc0ef 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -19,7 +19,8 @@ import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -describe('migration v2', () => { +// FLAKY: https://github.com/elastic/kibana/issues/91107 +describe.skip('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let coreStart: InternalCoreStart; diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index 99c06c0a3586b..d4ce7b74baa5f 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -206,6 +206,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] INIT -> LEGACY_DELETE", Object { + "batchSize": 1000, "controlState": "LEGACY_DELETE", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", @@ -262,6 +263,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] LEGACY_DELETE -> FATAL", Object { + "batchSize": 1000, "controlState": "FATAL", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", @@ -413,6 +415,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] INIT -> LEGACY_REINDEX", Object { + "batchSize": 1000, "controlState": "LEGACY_REINDEX", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", @@ -464,6 +467,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE", Object { + "batchSize": 1000, "controlState": "LEGACY_DELETE", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 2813f01093e95..f9bf3418c0ab6 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -46,6 +46,7 @@ describe('migrations v2 model', () => { retryCount: 0, retryDelay: 0, retryAttempts: 15, + batchSize: 1000, indexPrefix: '.kibana', outdatedDocumentsQuery: {}, targetIndexMappings: { @@ -1182,6 +1183,7 @@ describe('migrations v2 model', () => { describe('createInitialState', () => { const migrationsConfig = ({ retryAttempts: 15, + batchSize: 1000, } as unknown) as SavedObjectsMigrationConfigType; it('creates the initial state for the model based on the passed in paramaters', () => { expect( @@ -1197,6 +1199,7 @@ describe('migrations v2 model', () => { }) ).toMatchInlineSnapshot(` Object { + "batchSize": 1000, "controlState": "INIT", "currentAlias": ".kibana_task_manager", "indexPrefix": ".kibana_task_manager", diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 5bdba98026792..e62bd108faea0 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -784,6 +784,7 @@ export const createInitialState = ({ retryCount: 0, retryDelay: 0, retryAttempts: migrationsConfig.retryAttempts, + batchSize: migrationsConfig.batchSize, logs: [], }; return initialState; diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 1b594cf3d8b53..5c159f4f24e22 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -73,7 +73,11 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK: (state: UpdateTargetMappingsWaitForTaskState) => Actions.waitForPickupUpdatedMappingsTask(client, state.updateTargetMappingsTaskId, '60s'), OUTDATED_DOCUMENTS_SEARCH: (state: OutdatedDocumentsSearch) => - Actions.searchForOutdatedDocuments(client, state.targetIndex, state.outdatedDocumentsQuery), + Actions.searchForOutdatedDocuments(client, { + batchSize: state.batchSize, + targetIndex: state.targetIndex, + outdatedDocumentsQuery: state.outdatedDocumentsQuery, + }), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => pipe( TaskEither.tryCatch( diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index dbdd5774dfa62..8d6fe3f030eb3 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -54,6 +54,21 @@ export interface BaseState extends ControlState { * max_retry_time = 11.7 minutes */ readonly retryAttempts: number; + + /** + * The number of documents to fetch from Elasticsearch server to run migration over. + * + * The higher the value, the faster the migration process will be performed since it reduces + * the number of round trips between Kibana and Elasticsearch servers. + * For the migration speed, we have to pay the price of increased memory consumption. + * + * Since batchSize defines the number of documents, not their size, it might happen that + * Elasticsearch fails a request with circuit_breaking_exception when it retrieves a set of + * saved objects of significant size. + * + * In this case, you should set a smaller batchSize value and restart the migration process again. + */ + readonly batchSize: number; readonly logs: Array<{ level: 'error' | 'info'; message: string }>; /** * The current alias e.g. `.kibana` which always points to the latest diff --git a/src/core/server/saved_objects/saved_objects_config.test.ts b/src/core/server/saved_objects/saved_objects_config.test.ts new file mode 100644 index 0000000000000..720b28403edf2 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_config.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { savedObjectsMigrationConfig } from './saved_objects_config'; +import { getDeprecationsFor } from '../config/test_utils'; + +const applyMigrationsDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: savedObjectsMigrationConfig.deprecations!, + settings, + path: 'migrations', + }); + +describe('migrations config', function () { + describe('deprecations', () => { + it('logs a warning if migrations.enableV2 is set: true', () => { + const { messages } = applyMigrationsDeprecations({ enableV2: true }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"migrations.enableV2\\" is deprecated and will be removed in an upcoming release without any further notice.", + ] + `); + }); + + it('logs a warning if migrations.enableV2 is set: false', () => { + const { messages } = applyMigrationsDeprecations({ enableV2: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"migrations.enableV2\\" is deprecated and will be removed in an upcoming release without any further notice.", + ] + `); + }); + }); + + it('does not log a warning if migrations.enableV2 is not set', () => { + const { messages } = applyMigrationsDeprecations({ batchSize: 1_000 }); + expect(messages).toMatchInlineSnapshot(`Array []`); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 7228cb126d286..7182df74c597f 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -7,31 +7,50 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import type { ServiceConfigDescriptor } from '../internal_types'; +import type { ConfigDeprecationProvider } from '../config'; -export type SavedObjectsMigrationConfigType = TypeOf; +const migrationSchema = schema.object({ + batchSize: schema.number({ defaultValue: 1_000 }), + scrollDuration: schema.string({ defaultValue: '15m' }), + pollInterval: schema.number({ defaultValue: 1_500 }), + skip: schema.boolean({ defaultValue: false }), + enableV2: schema.boolean({ defaultValue: true }), + retryAttempts: schema.number({ defaultValue: 15 }), +}); -export const savedObjectsMigrationConfig = { +export type SavedObjectsMigrationConfigType = TypeOf; + +const migrationDeprecations: ConfigDeprecationProvider = () => [ + (settings, fromPath, addDeprecation) => { + const migrationsConfig = settings[fromPath]; + if (migrationsConfig?.enableV2 !== undefined) { + addDeprecation({ + message: + '"migrations.enableV2" is deprecated and will be removed in an upcoming release without any further notice.', + documentationUrl: 'https://ela.st/kbn-so-migration-v2', + }); + } + return settings; + }, +]; + +export const savedObjectsMigrationConfig: ServiceConfigDescriptor = { path: 'migrations', - schema: schema.object({ - batchSize: schema.number({ defaultValue: 1000 }), - scrollDuration: schema.string({ defaultValue: '15m' }), - pollInterval: schema.number({ defaultValue: 1500 }), - skip: schema.boolean({ defaultValue: false }), - // TODO migrationsV2: remove/deprecate once we release migrations v2 - enableV2: schema.boolean({ defaultValue: true }), - /** the number of times v2 migrations will retry temporary failures such as a timeout, 503 status code or snapshot_in_progress_exception */ - retryAttempts: schema.number({ defaultValue: 15 }), - }), + schema: migrationSchema, + deprecations: migrationDeprecations, }; -export type SavedObjectsConfigType = TypeOf; +const soSchema = schema.object({ + maxImportPayloadBytes: schema.byteSize({ defaultValue: 26_214_400 }), + maxImportExportSize: schema.number({ defaultValue: 10_000 }), +}); + +export type SavedObjectsConfigType = TypeOf; -export const savedObjectsConfig = { +export const savedObjectsConfig: ServiceConfigDescriptor = { path: 'savedObjects', - schema: schema.object({ - maxImportPayloadBytes: schema.byteSize({ defaultValue: 26214400 }), - maxImportExportSize: schema.number({ defaultValue: 10000 }), - }), + schema: soSchema, }; export class SavedObjectConfig { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index de96c5ccfb81e..53b2eb8610418 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -142,7 +142,6 @@ import { SearchParams } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; -import { Server } from '@hapi/hapi'; import { ShallowPromise } from '@kbn/utility-types'; import { SnapshotCreateParams } from 'elasticsearch'; import { SnapshotCreateRepositoryParams } from 'elasticsearch'; @@ -345,7 +344,7 @@ export const config: { pingTimeout: Type; logQueries: Type; ssl: import("@kbn/config-schema").ObjectType<{ - verificationMode: Type<"certificate" | "none" | "full">; + verificationMode: Type<"none" | "certificate" | "full">; certificateAuthorities: Type; certificate: Type; key: Type; @@ -1305,10 +1304,10 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory; // @public export const kibanaResponseFactory: { - custom: | Error | Buffer | { + custom: | Error | Buffer | Stream | { message: string | Error; attributes?: Record | undefined; - } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; @@ -1585,20 +1584,6 @@ export class LegacyClusterClient implements ILegacyClusterClient { close(): void; } -// @internal @deprecated -export interface LegacyConfig { - // (undocumented) - get(key?: string): T; - // (undocumented) - has(key: string): boolean; - // (undocumented) - set(key: string, value: any): void; - // Warning: (ae-forgotten-export) The symbol "LegacyVars" needs to be exported by the entry point index.d.ts - // - // (undocumented) - set(config: LegacyVars): void; -} - // @public @deprecated (undocumented) export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; @@ -1634,30 +1619,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// @public @deprecated (undocumented) -export interface LegacyServiceSetupDeps { - // Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: LegacyCoreSetup; - // (undocumented) - plugins: Record; - // Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts - // - // (undocumented) - uiPlugins: UiPlugins; -} - -// @public @deprecated (undocumented) -export interface LegacyServiceStartDeps { - // Warning: (ae-forgotten-export) The symbol "LegacyCoreStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: LegacyCoreStart; - // (undocumented) - plugins: Record; -} - // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // // @public @@ -3259,9 +3220,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:296:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:401:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 96047dc6921ec..2bd3028b2f1b6 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -58,7 +58,7 @@ jest.doMock('./ui_settings/ui_settings_service', () => ({ })); export const mockEnsureValidConfiguration = jest.fn(); -jest.doMock('./legacy/config/ensure_valid_configuration', () => ({ +jest.doMock('./config/ensure_valid_configuration', () => ({ ensureValidConfiguration: mockEnsureValidConfiguration, })); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index fcf09b0295bcb..534d7df9d9466 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -99,7 +99,6 @@ test('injects legacy dependency to context#setup()', async () => { pluginDependencies: new Map([ [pluginA, []], [pluginB, [pluginA]], - [mockLegacyService.legacyId, [pluginA, pluginB]], ]), }); }); @@ -108,12 +107,10 @@ test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.start).not.toHaveBeenCalled(); await server.setup(); expect(mockHttpService.start).not.toHaveBeenCalled(); - expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); @@ -121,7 +118,6 @@ test('runs services on "start"', async () => { await server.start(); expect(mockHttpService.start).toHaveBeenCalledTimes(1); - expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); @@ -164,26 +160,6 @@ test('stops services on "stop"', async () => { }); test(`doesn't setup core services if config validation fails`, async () => { - mockConfigService.validate.mockImplementationOnce(() => { - return Promise.reject(new Error('invalid config')); - }); - const server = new Server(rawConfigService, env, logger); - await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid config"`); - - expect(mockHttpService.setup).not.toHaveBeenCalled(); - expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); - expect(mockPluginsService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.setup).not.toHaveBeenCalled(); - expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); - expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); - expect(mockRenderingService.setup).not.toHaveBeenCalled(); - expect(mockMetricsService.setup).not.toHaveBeenCalled(); - expect(mockStatusService.setup).not.toHaveBeenCalled(); - expect(mockLoggingService.setup).not.toHaveBeenCalled(); - expect(mockI18nService.setup).not.toHaveBeenCalled(); -}); - -test(`doesn't setup core services if legacy config validation fails`, async () => { mockEnsureValidConfiguration.mockImplementation(() => { throw new Error('Unknown configuration keys'); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index b575b2779082c..b34d7fec3dcbf 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -8,15 +8,20 @@ import apm from 'elastic-apm-node'; import { config as pathConfig } from '@kbn/utils'; -import { mapToObject } from '@kbn/std'; -import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; +import { + ConfigService, + Env, + RawConfigurationProvider, + coreDeprecationProvider, + ensureValidConfiguration, +} from './config'; import { CoreApp } from './core_app'; import { I18nService } from './i18n'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; import { RenderingService } from './rendering'; -import { LegacyService, ensureValidConfiguration } from './legacy'; +import { LegacyService } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; @@ -121,22 +126,13 @@ export class Server { const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); - const legacyConfigSetup = await this.legacy.setupLegacyConfig(); // Immediately terminate in case of invalid configuration // This needs to be done after plugin discovery - await this.configService.validate(); - await ensureValidConfiguration(this.configService, legacyConfigSetup); + await ensureValidConfiguration(this.configService); const contextServiceSetup = this.context.setup({ - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any KP plugin - // 2) Can register context providers that will only be available to other legacy plugins and will not leak into - // New Platform plugins. - pluginDependencies: new Map([ - ...pluginTree.asOpaqueIds, - [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], - ]), + pluginDependencies: new Map([...pluginTree.asOpaqueIds]), }); const httpSetup = await this.http.setup({ @@ -222,9 +218,7 @@ export class Server { this.#pluginsInitialized = pluginsSetup.initialized; await this.legacy.setup({ - core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup }, - plugins: mapToObject(pluginsSetup.contracts), - uiPlugins, + http: httpSetup, }); this.registerCoreContext(coreSetup); @@ -266,15 +260,7 @@ export class Server { coreUsageData: coreUsageDataStart, }; - const pluginsStart = await this.plugins.start(this.coreStart); - - await this.legacy.start({ - core: { - ...this.coreStart, - plugins: pluginsStart, - }, - plugins: mapToObject(pluginsStart.contracts), - }); + await this.plugins.start(this.coreStart); await this.http.start(); diff --git a/src/core/server/test_utils.ts b/src/core/server/test_utils.ts index 656d2bfe60fac..cf18defb0a960 100644 --- a/src/core/server/test_utils.ts +++ b/src/core/server/test_utils.ts @@ -9,3 +9,4 @@ export { createHttpServer } from './http/test_utils'; export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils'; export { setupServer } from './saved_objects/routes/test_utils'; +export { getDeprecationsFor, getDeprecationsForGlobalSettings } from './config/test_utils'; diff --git a/src/core/server/types.ts b/src/core/server/types.ts index ab1d6c6d95d0a..be07a3cfb1fd3 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -39,6 +39,5 @@ export type { } from './saved_objects/types'; export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types'; export * from './ui_settings/types'; -export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; export type { ExternalUrlConfig, IExternalUrlPolicy } from './external_url'; diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index 86a9a24fab6de..59c27cc136174 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -9,10 +9,10 @@ import { getServices, chance } from './lib'; export const docExistsSuite = (savedObjectsIndex: string) => () => { - async function setup(options: any = {}) { + async function setup(options: { initialSettings?: Record } = {}) { const { initialSettings } = options; - const { kbnServer, uiSettings, callCluster } = getServices(); + const { uiSettings, callCluster, supertest } = getServices(); // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { @@ -21,31 +21,30 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { conflicts: 'proceed', query: { match_all: {} }, }, + refresh: true, + wait_for_completion: true, }); if (initialSettings) { await uiSettings.setMany(initialSettings); } - return { kbnServer, uiSettings }; + return { uiSettings, supertest }; } describe('get route', () => { it('returns a 200 and includes userValues', async () => { const defaultIndex = chance.word({ length: 10 }); - const { kbnServer } = await setup({ + + const { supertest } = await setup({ initialSettings: { defaultIndex, }, }); - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); + const { body } = await supertest('get', '/api/kibana/settings').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -64,20 +63,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { describe('set route', () => { it('returns a 200 and all values including update', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { - value: defaultIndex, - }, - }); - expect(statusCode).toBe(200); + const { body } = await supertest('post', '/api/kibana/settings/defaultIndex') + .send({ + value: defaultIndex, + }) + .expect(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -94,18 +90,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); it('returns a 400 if trying to set overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/foo', - payload: { + const { body } = await supertest('delete', '/api/kibana/settings/foo') + .send({ value: 'baz', - }, - }); + }) + .expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -115,22 +108,18 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { describe('setMany route', () => { it('returns a 200 and all values including updates', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { defaultIndex, }, - }, - }); + }) + .expect(200); - expect(statusCode).toBe(200); - - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -147,20 +136,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); it('returns a 400 if trying to set overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { foo: 'baz', }, - }, - }); + }) + .expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -172,19 +158,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { it('returns a 200 and deletes the setting', async () => { const defaultIndex = chance.word({ length: 10 }); - const { kbnServer, uiSettings } = await setup({ + const { uiSettings, supertest } = await setup({ initialSettings: { defaultIndex }, }); expect(await uiSettings.get('defaultIndex')).toBe(defaultIndex); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); + const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -197,15 +179,11 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); }); it('returns a 400 if deleting overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/foo', - }); + const { body } = await supertest('delete', '/api/kibana/settings/foo').expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index 9fa3e4c1cfe78..29d1daf3b2032 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -11,14 +11,7 @@ import { getServices, chance } from './lib'; export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { - const { kbnServer, callCluster } = getServices(); - - // write a setting to ensure kibana index is created - await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' }, - }); + const { callCluster } = getServices(); // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { @@ -31,15 +24,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('get route', () => { it('creates doc, returns a 200 with settings', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); + const { body } = await supertest('get', '/api/kibana/settings').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -55,17 +44,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('set route', () => { it('creates doc, returns a 200 with value set', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex }, - }); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + const { body } = await supertest('post', '/api/kibana/settings/defaultIndex') + .send({ + value: defaultIndex, + }) + .expect(200); + + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -84,19 +73,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('setMany route', () => { it('creates doc, returns 200 with updated values', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { defaultIndex }, - }, - }); + }) + .expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -115,15 +102,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('delete route', () => { it('creates doc, returns a 200 with just buildNum', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); + const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts deleted file mode 100644 index 78fdab7eb8c5d..0000000000000 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ /dev/null @@ -1,145 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getServices, chance } from './lib'; - -export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { - // ensure the kibana index has no documents - beforeEach(async () => { - const { kbnServer, callCluster } = getServices(); - - // write a setting to ensure kibana index is created - await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' }, - }); - - // delete all docs from kibana index to ensure savedConfig is not found - await callCluster('deleteByQuery', { - index: savedObjectsIndex, - body: { - query: { match_all: {} }, - }, - }); - - // set the index to read only - await callCluster('indices.putSettings', { - index: savedObjectsIndex, - body: { - index: { - blocks: { - read_only: true, - }, - }, - }, - }); - }); - - afterEach(async () => { - const { callCluster } = getServices(); - - // disable the read only block - await callCluster('indices.putSettings', { - index: savedObjectsIndex, - body: { - index: { - blocks: { - read_only: false, - }, - }, - }, - }); - }); - - describe('get route', () => { - it('returns simulated doc with buildNum', async () => { - const { kbnServer } = getServices(); - - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); - - expect(statusCode).toBe(200); - - expect(result).toMatchObject({ - settings: { - buildNum: { - userValue: expect.any(Number), - }, - foo: { - userValue: 'bar', - isOverridden: true, - }, - }, - }); - }); - }); - - describe('set route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex }, - }); - - expect(statusCode).toBe(403); - - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); - - describe('setMany route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { - changes: { defaultIndex }, - }, - }); - - expect(statusCode).toBe(403); - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); - - describe('delete route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); - - expect(statusCode).toBe(403); - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); -}; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 6e6c357e6cccc..6c7cdfa43cf57 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -12,7 +12,6 @@ import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; -import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; const savedObjectIndex = `.kibana_${kibanaVersion}_001`; @@ -23,7 +22,6 @@ describe('uiSettings/routes', function () { beforeAll(startServers); /* eslint-disable jest/valid-describe */ describe('doc missing', docMissingSuite(savedObjectIndex)); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 87176bed5de11..d019dc640f385 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type supertest from 'supertest'; import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server'; import { @@ -13,6 +14,8 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, + HttpMethod, + getSupertest, } from '../../../../test_helpers/kbn_server'; import { LegacyAPICaller } from '../../../elasticsearch/'; import { httpServerMock } from '../../../http/http_server.mocks'; @@ -21,13 +24,11 @@ let servers: TestUtils; let esServer: TestElasticsearchUtils; let kbn: TestKibanaUtils; -let kbnServer: TestKibanaUtils['kbnServer']; - interface AllServices { - kbnServer: TestKibanaUtils['kbnServer']; savedObjectsClient: SavedObjectsClientContract; callCluster: LegacyAPICaller; uiSettings: IUiSettingsClient; + supertest: (method: HttpMethod, path: string) => supertest.Test; } let services: AllServices; @@ -47,7 +48,6 @@ export async function startServers() { }); esServer = await servers.startES(); kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; } export function getServices() { @@ -61,12 +61,10 @@ export function getServices() { httpServerMock.createKibanaRequest() ); - const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( - savedObjectsClient - ); + const uiSettings = kbn.coreStart.uiSettings.asScopedToClient(savedObjectsClient); services = { - kbnServer, + supertest: (method: HttpMethod, path: string) => getSupertest(kbn.root, method, path), callCluster, savedObjectsClient, uiSettings, @@ -77,7 +75,6 @@ export function getServices() { export async function stopServers() { services = null!; - kbnServer = null!; if (servers) { await esServer.stop(); await kbn.stop(); diff --git a/src/core/server/ui_settings/saved_objects/migrations.test.ts b/src/core/server/ui_settings/saved_objects/migrations.test.ts index cf96372bd20bc..cb10f9c7fd981 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.test.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.test.ts @@ -76,6 +76,23 @@ describe('ui_settings 7.12.0 migrations', () => { const migrated = migration(doc); expect(JSON.parse(migrated.attributes['timepicker:quickRanges'])).toEqual([migratedQuickRange]); }); + + // https://github.com/elastic/kibana/issues/95616 + test('returns doc when "timepicker:quickRanges" is null', () => { + const doc = { + type: 'config', + id: '8.0.0', + attributes: { + buildNum: 9007199254740991, + 'timepicker:quickRanges': null, + }, + references: [], + updated_at: '2020-06-09T20:18:20.349Z', + migrationVersion: {}, + }; + const migrated = migration(doc); + expect(migrated).toEqual(doc); + }); }); describe('ui_settings 7.13.0 migrations', () => { diff --git a/src/core/server/ui_settings/saved_objects/migrations.ts b/src/core/server/ui_settings/saved_objects/migrations.ts index 16f217352b99a..b187c5f86dab0 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.ts @@ -32,7 +32,7 @@ export const migrations = { ...doc, ...(doc.attributes && { attributes: Object.keys(doc.attributes).reduce((acc, key) => { - if (key === 'timepicker:quickRanges' && doc.attributes[key].indexOf('section') > -1) { + if (key === 'timepicker:quickRanges' && doc.attributes[key]?.indexOf('section') > -1) { const ranges = JSON.parse(doc.attributes[key]).map( ({ from, to, display }: { from: string; to: string; display: string }) => { return { diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 1844b5de3dc35..950ab5f4392e1 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -29,11 +29,10 @@ import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; -import { CoreStart } from 'src/core/server'; +import { InternalCoreSetup, InternalCoreStart } from '../server/internal_types'; import { LegacyAPICaller } from '../server/elasticsearch'; import { CliArgs, Env } from '../server/config'; import { Root } from '../server/root'; -import KbnServer from '../../legacy/server/kbn_server'; export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -125,14 +124,6 @@ export function createRootWithCorePlugins(settings = {}, cliArgs: Partial ReturnType @@ -164,8 +155,8 @@ export interface TestElasticsearchUtils { export interface TestKibanaUtils { root: Root; - coreStart: CoreStart; - kbnServer: KbnServer; + coreSetup: InternalCoreSetup; + coreStart: InternalCoreStart; stop: () => Promise; } @@ -283,14 +274,12 @@ export function createTestServers({ startKibana: async () => { const root = createRootWithCorePlugins(kbnSettings); - await root.setup(); + const coreSetup = await root.setup(); const coreStart = await root.start(); - const kbnServer = getKbnServer(root); - return { root, - kbnServer, + coreSetup, coreStart, stop: async () => await root.shutdown(), }; diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index f19e379482d3c..855962070457e 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.project.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 9617a556e2cdd..6cc94208fbcce 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -163,7 +163,11 @@ kibana_vars=( xpack.actions.proxyHeaders xpack.actions.proxyRejectUnauthorizedCertificates xpack.actions.proxyUrl + xpack.actions.proxyBypassHosts + xpack.actions.proxyOnlyHosts xpack.actions.rejectUnauthorized + xpack.actions.maxResponseContentLength + xpack.actions.responseTimeout xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts index 3a296ec76f3e6..004e17b87ac8b 100644 --- a/src/dev/build/tasks/package_json/find_used_dependencies.ts +++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ +import Path from 'path'; import globby from 'globby'; +import normalize from 'normalize-path'; // @ts-ignore import { parseEntries, dependenciesParseStrategy } from '@kbn/babel-code-parser'; @@ -21,16 +23,16 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // Define the entry points for the server code in order to // start here later looking for the server side dependencies const mainCodeEntries = [ - `${baseDir}/src/cli/dist.js`, - `${baseDir}/src/cli_keystore/dist.js`, - `${baseDir}/src/cli_plugin/dist.js`, + Path.resolve(baseDir, `src/cli/dist.js`), + Path.resolve(baseDir, `src/cli_keystore/dist.js`), + Path.resolve(baseDir, `src/cli_plugin/dist.js`), ]; const discoveredPluginEntries = await globby([ - `${baseDir}/src/plugins/*/server/index.js`, - `!${baseDir}/src/plugins/**/public`, - `${baseDir}/x-pack/plugins/*/server/index.js`, - `!${baseDir}/x-pack/plugins/**/public`, + normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)), + `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`, + normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)), + `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`, ]); // It will include entries that cannot be discovered @@ -40,7 +42,7 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // Another way would be to include an index file and import all the functions // using named imports const dynamicRequiredEntries = await globby([ - `${baseDir}/src/plugins/vis_type_timelion/server/**/*.js`, + normalize(Path.resolve(baseDir, 'src/plugins/vis_type_timelion/server/**/*.js')), ]); // Compose all the needed entries diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 738b38ee28bde..bb98498e6d601 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -35,13 +35,13 @@ describe(`enumeratePatterns`, () => { 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app' ); }); - it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts to kibana-security`, () => { + it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts to kibana-security`, () => { const short = 'x-pack/plugins/security_solution'; const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); expect( actual[0].includes( - `${short}/public/common/components/exceptions/builder/translations.ts kibana-security` + `${short}/public/common/components/exceptions/edit_exception_modal/translations.ts kibana-security` ) ).toBe(true); }); diff --git a/src/dev/run_check_file_casing.js b/src/dev/run_check_file_casing.ts similarity index 92% rename from src/dev/run_check_file_casing.js rename to src/dev/run_check_file_casing.ts index 0add66dd272c8..554aa2418f579 100644 --- a/src/dev/run_check_file_casing.js +++ b/src/dev/run_check_file_casing.ts @@ -11,12 +11,13 @@ import globby from 'globby'; import { REPO_ROOT } from '@kbn/utils'; import { run } from '@kbn/dev-utils'; import { File } from './file'; +// @ts-expect-error precommit hooks aren't migrated to TypeScript yet. import { checkFileCasing } from './precommit_hook/check_file_casing'; run(async ({ log }) => { const paths = await globby('**/*', { cwd: REPO_ROOT, - nodir: true, + onlyFiles: true, gitignore: true, ignore: [ // the gitignore: true option makes sure that we don't diff --git a/src/dev/typescript/project.ts b/src/dev/typescript/project.ts index 04a5de945619b..8d92284e49637 100644 --- a/src/dev/typescript/project.ts +++ b/src/dev/typescript/project.ts @@ -7,7 +7,7 @@ */ import { basename, dirname, relative, resolve } from 'path'; -import { memoize } from 'lodash'; + import { IMinimatch, Minimatch } from 'minimatch'; import { REPO_ROOT } from '@kbn/utils'; @@ -26,10 +26,6 @@ function testMatchers(matchers: IMinimatch[], path: string) { return matchers.some((matcher) => matcher.match(path)); } -const parentProjectFactory = memoize(function (parentConfigPath: string) { - return new Project(parentConfigPath); -}); - export class Project { public directory: string; public name: string; @@ -38,7 +34,6 @@ export class Project { private readonly include: IMinimatch[]; private readonly exclude: IMinimatch[]; - private readonly parent?: Project; constructor( public tsConfigPath: string, @@ -46,16 +41,15 @@ export class Project { ) { this.config = parseTsConfig(tsConfigPath); - const { files, include, exclude = [], extends: extendsPath } = this.config as { + const { files, include, exclude = [] } = this.config as { files?: string[]; include?: string[]; exclude?: string[]; - extends?: string; }; if (files || !include) { throw new Error( - `[${tsConfigPath}]: tsconfig.json files in the Kibana repo must use "include" keys and not "files"` + 'tsconfig.json files in the Kibana repo must use "include" keys and not "files"' ); } @@ -64,30 +58,9 @@ export class Project { this.name = options.name || relative(REPO_ROOT, this.directory) || basename(this.directory); this.include = makeMatchers(this.directory, include); this.exclude = makeMatchers(this.directory, exclude); - - if (extendsPath !== undefined) { - const parentConfigPath = resolve(this.directory, extendsPath); - this.parent = parentProjectFactory(parentConfigPath); - } - } - - public isAbsolutePathSelected(path: string): boolean { - return this.isExcluded(path) ? false : this.isIncluded(path); } - public isExcluded(path: string): boolean { - if (testMatchers(this.exclude, path)) return true; - if (this.parent) { - return this.parent.isExcluded(path); - } - return false; - } - - public isIncluded(path: string): boolean { - if (testMatchers(this.include, path)) return true; - if (this.parent) { - return this.parent.isIncluded(path); - } - return false; + public isAbsolutePathSelected(path: string) { + return testMatchers(this.exclude, path) ? false : testMatchers(this.include, path); } } diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts index 2bc75785ee6a7..7347529239176 100644 --- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts +++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts @@ -12,6 +12,7 @@ import Fs from 'fs'; import del from 'del'; import cpy from 'cpy'; import globby from 'globby'; +import normalize from 'normalize-path'; import { ToolingLog, createAbsolutePathSerializer, @@ -98,7 +99,10 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and const files = Object.fromEntries( globby - .sync(outDirs, { dot: true }) + .sync( + outDirs.map((p) => normalize(p)), + { dot: true } + ) .map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')]) ); diff --git a/src/legacy/server/config/__snapshots__/config.test.js.snap b/src/legacy/server/config/__snapshots__/config.test.js.snap deleted file mode 100644 index 3bf471f8aba20..0000000000000 --- a/src/legacy/server/config/__snapshots__/config.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`lib/config/config class Config() #getDefault(key) array key should throw exception for unknown key 1`] = `"Unknown config key: foo,bar."`; - -exports[`lib/config/config class Config() #getDefault(key) dot notation key should throw exception for unknown key 1`] = `"Unknown config key: foo.bar."`; diff --git a/src/legacy/server/config/config.js b/src/legacy/server/config/config.js deleted file mode 100644 index 81cb0a36333bd..0000000000000 --- a/src/legacy/server/config/config.js +++ /dev/null @@ -1,207 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Joi from 'joi'; -import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; -import { override } from './override'; -import createDefaultSchema from './schema'; -import { unset, deepCloneWithBuffers as clone, IS_KIBANA_DISTRIBUTABLE } from '../../utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { pkg } from '../../../core/server/utils'; -const schema = Symbol('Joi Schema'); -const schemaExts = Symbol('Schema Extensions'); -const vals = Symbol('config values'); - -export class Config { - static withDefaultSchema(settings = {}) { - const defaultSchema = createDefaultSchema(); - return new Config(defaultSchema, settings); - } - - constructor(initialSchema, initialSettings) { - this[schemaExts] = Object.create(null); - this[vals] = Object.create(null); - - this.extendSchema(initialSchema, initialSettings); - } - - extendSchema(extension, settings, key) { - if (!extension) { - return; - } - - if (!key) { - return _.each(extension._inner.children, (child) => { - this.extendSchema(child.schema, _.get(settings, child.key), child.key); - }); - } - - if (this.has(key)) { - throw new Error(`Config schema already has key: ${key}`); - } - - set(this[schemaExts], key, extension); - this[schema] = null; - - this.set(key, settings); - } - - removeSchema(key) { - if (!_.has(this[schemaExts], key)) { - throw new TypeError(`Unknown schema key: ${key}`); - } - - this[schema] = null; - unset(this[schemaExts], key); - unset(this[vals], key); - } - - resetTo(obj) { - this._commit(obj); - } - - set(key, value) { - // clone and modify the config - let config = clone(this[vals]); - if (_.isPlainObject(key)) { - config = override(config, key); - } else { - set(config, key, value); - } - - // attempt to validate the config value - this._commit(config); - } - - _commit(newVals) { - // resolve the current environment - let env = newVals.env; - delete newVals.env; - if (_.isObject(env)) env = env.name; - if (!env) env = 'production'; - - const dev = env === 'development'; - const prod = env === 'production'; - - // pass the environment as context so that it can be refed in config - const context = { - env: env, - prod: prod, - dev: dev, - notProd: !prod, - notDev: !dev, - version: _.get(pkg, 'version'), - branch: _.get(pkg, 'branch'), - buildNum: IS_KIBANA_DISTRIBUTABLE ? pkg.build.number : Number.MAX_SAFE_INTEGER, - buildSha: IS_KIBANA_DISTRIBUTABLE - ? pkg.build.sha - : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - dist: IS_KIBANA_DISTRIBUTABLE, - }; - - if (!context.dev && !context.prod) { - throw new TypeError( - `Unexpected environment "${env}", expected one of "development" or "production"` - ); - } - - const results = Joi.validate(newVals, this.getSchema(), { - context, - abortEarly: false, - }); - - if (results.error) { - const error = new Error(results.error.message); - error.name = results.error.name; - error.stack = results.error.stack; - throw error; - } - - this[vals] = results.value; - } - - get(key) { - if (!key) { - return clone(this[vals]); - } - - const value = _.get(this[vals], key); - if (value === undefined) { - if (!this.has(key)) { - throw new Error('Unknown config key: ' + key); - } - } - return clone(value); - } - - getDefault(key) { - const schemaKey = Array.isArray(key) ? key.join('.') : key; - - const subSchema = Joi.reach(this.getSchema(), schemaKey); - if (!subSchema) { - throw new Error(`Unknown config key: ${key}.`); - } - - return clone(_.get(Joi.describe(subSchema), 'flags.default')); - } - - has(key) { - function has(key, schema, path) { - path = path || []; - // Catch the partial paths - if (path.join('.') === key) return true; - // Only go deep on inner objects with children - if (_.size(schema._inner.children)) { - for (let i = 0; i < schema._inner.children.length; i++) { - const child = schema._inner.children[i]; - // If the child is an object recurse through it's children and return - // true if there's a match - if (child.schema._type === 'object') { - if (has(key, child.schema, path.concat([child.key]))) return true; - // if the child matches, return true - } else if (path.concat([child.key]).join('.') === key) { - return true; - } - } - } - } - - if (Array.isArray(key)) { - // TODO: add .has() support for array keys - key = key.join('.'); - } - - return !!has(key, this.getSchema()); - } - - getSchema() { - if (!this[schema]) { - this[schema] = (function convertToSchema(children) { - let schema = Joi.object().keys({}).default(); - - for (const key of Object.keys(children)) { - const child = children[key]; - const childSchema = _.isPlainObject(child) ? convertToSchema(child) : child; - - if (!childSchema || !childSchema.isJoi) { - throw new TypeError( - 'Unable to convert configuration definition value to Joi schema: ' + childSchema - ); - } - - schema = schema.keys({ [key]: childSchema }); - } - - return schema; - })(this[schemaExts]); - } - - return this[schema]; - } -} diff --git a/src/legacy/server/config/config.test.js b/src/legacy/server/config/config.test.js deleted file mode 100644 index b617babb8262d..0000000000000 --- a/src/legacy/server/config/config.test.js +++ /dev/null @@ -1,345 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Config } from './config'; -import _ from 'lodash'; -import Joi from 'joi'; - -/** - * Plugins should defined a config method that takes a joi object. By default - * it should return a way to disallow config - * - * Config should be newed up with a joi schema (containing defaults via joi) - * - * let schema = { ... } - * new Config(schema); - * - */ - -const data = { - test: { - hosts: ['host-01', 'host-02'], - client: { - type: 'datastore', - host: 'store-01', - port: 5050, - }, - }, -}; - -const schema = Joi.object({ - test: Joi.object({ - enable: Joi.boolean().default(true), - hosts: Joi.array().items(Joi.string()), - client: Joi.object({ - type: Joi.string().default('datastore'), - host: Joi.string(), - port: Joi.number(), - }).default(), - undefValue: Joi.string(), - }).default(), -}).default(); - -describe('lib/config/config', function () { - describe('class Config()', function () { - describe('constructor', function () { - it('should not allow any config if the schema is not passed', function () { - const config = new Config(); - const run = function () { - config.set('something.enable', true); - }; - expect(run).toThrow(); - }); - - it('should allow keys in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('test.client.host', 'http://localhost'); - }; - expect(run).not.toThrow(); - }); - - it('should not allow keys not in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('paramNotDefinedInTheSchema', true); - }; - expect(run).toThrow(); - }); - - it('should not allow child keys not in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('test.client.paramNotDefinedInTheSchema', true); - }; - expect(run).toThrow(); - }); - - it('should set defaults', function () { - const config = new Config(schema); - expect(config.get('test.enable')).toBe(true); - expect(config.get('test.client.type')).toBe('datastore'); - }); - }); - - describe('#resetTo(object)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should reset the config object with new values', function () { - config.set(data); - const newData = config.get(); - newData.test.enable = false; - config.resetTo(newData); - expect(config.get()).toEqual(newData); - }); - }); - - describe('#has(key)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should return true for fields that exist in the schema', function () { - expect(config.has('test.undefValue')).toBe(true); - }); - - it('should return true for partial objects that exist in the schema', function () { - expect(config.has('test.client')).toBe(true); - }); - - it('should return false for fields that do not exist in the schema', function () { - expect(config.has('test.client.pool')).toBe(false); - }); - }); - - describe('#set(key, value)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - }); - - it('should use a key and value to set a config value', function () { - config.set('test.enable', false); - expect(config.get('test.enable')).toBe(false); - }); - - it('should use an object to set config values', function () { - const hosts = ['host-01', 'host-02']; - config.set({ test: { enable: false, hosts: hosts } }); - expect(config.get('test.enable')).toBe(false); - expect(config.get('test.hosts')).toEqual(hosts); - }); - - it('should use a flatten object to set config values', function () { - const hosts = ['host-01', 'host-02']; - config.set({ 'test.enable': false, 'test.hosts': hosts }); - expect(config.get('test.enable')).toBe(false); - expect(config.get('test.hosts')).toEqual(hosts); - }); - - it('should override values with just the values present', function () { - const newData = _.cloneDeep(data); - config.set(data); - newData.test.enable = false; - config.set({ test: { enable: false } }); - expect(config.get()).toEqual(newData); - }); - - it('should thow an exception when setting a value with the wrong type', function (done) { - expect.assertions(4); - - const run = function () { - config.set('test.enable', 'something'); - }; - - try { - run(); - } catch (err) { - expect(err).toHaveProperty('name', 'ValidationError'); - expect(err).toHaveProperty( - 'message', - 'child "test" fails because [child "enable" fails because ["enable" must be a boolean]]' - ); - expect(err).not.toHaveProperty('details'); - expect(err).not.toHaveProperty('_object'); - } - - done(); - }); - }); - - describe('#get(key)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - config.set(data); - }); - - it('should return the whole config object when called without a key', function () { - const newData = _.cloneDeep(data); - newData.test.enable = true; - expect(config.get()).toEqual(newData); - }); - - it('should return the value using dot notation', function () { - expect(config.get('test.enable')).toBe(true); - }); - - it('should return the clone of partial object using dot notation', function () { - expect(config.get('test.client')).not.toBe(data.test.client); - expect(config.get('test.client')).toEqual(data.test.client); - }); - - it('should throw exception for unknown config values', function () { - const run = function () { - config.get('test.does.not.exist'); - }; - expect(run).toThrowError(/Unknown config key: test.does.not.exist/); - }); - - it('should not throw exception for undefined known config values', function () { - const run = function getUndefValue() { - config.get('test.undefValue'); - }; - expect(run).not.toThrow(); - }); - }); - - describe('#getDefault(key)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - config.set(data); - }); - - describe('dot notation key', function () { - it('should return undefined if there is no default', function () { - const hostDefault = config.getDefault('test.client.host'); - expect(hostDefault).toBeUndefined(); - }); - - it('should return default if specified', function () { - const typeDefault = config.getDefault('test.client.type'); - expect(typeDefault).toBe('datastore'); - }); - - it('should throw exception for unknown key', function () { - expect(() => { - config.getDefault('foo.bar'); - }).toThrowErrorMatchingSnapshot(); - }); - }); - - describe('array key', function () { - it('should return undefined if there is no default', function () { - const hostDefault = config.getDefault(['test', 'client', 'host']); - expect(hostDefault).toBeUndefined(); - }); - - it('should return default if specified', function () { - const typeDefault = config.getDefault(['test', 'client', 'type']); - expect(typeDefault).toBe('datastore'); - }); - - it('should throw exception for unknown key', function () { - expect(() => { - config.getDefault(['foo', 'bar']); - }).toThrowErrorMatchingSnapshot(); - }); - }); - - it('object schema with no default should return default value for property', function () { - const noDefaultSchema = Joi.object() - .keys({ - foo: Joi.array().items(Joi.string().min(1)).default(['bar']), - }) - .required(); - - const config = new Config(noDefaultSchema); - config.set({ - foo: ['baz'], - }); - - const fooDefault = config.getDefault('foo'); - expect(fooDefault).toEqual(['bar']); - }); - - it('should return clone of the default', function () { - const schemaWithArrayDefault = Joi.object() - .keys({ - foo: Joi.array().items(Joi.string().min(1)).default(['bar']), - }) - .default(); - - const config = new Config(schemaWithArrayDefault); - config.set({ - foo: ['baz'], - }); - - expect(config.getDefault('foo')).not.toBe(config.getDefault('foo')); - expect(config.getDefault('foo')).toEqual(config.getDefault('foo')); - }); - }); - - describe('#extendSchema(key, schema)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should allow you to extend the schema at the top level', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema(newSchema, {}, 'myTest'); - expect(config.get('myTest.test')).toBe(true); - }); - - it('should allow you to extend the schema with a prefix', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema(newSchema, {}, 'prefix.myTest'); - expect(config.get('prefix')).toEqual({ myTest: { test: true } }); - expect(config.get('prefix.myTest')).toEqual({ test: true }); - expect(config.get('prefix.myTest.test')).toBe(true); - }); - - it('should NOT allow you to extend the schema if something else is there', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - const run = function () { - config.extendSchema('test', newSchema); - }; - expect(run).toThrow(); - }); - }); - - describe('#removeSchema(key)', function () { - it('should completely remove the key', function () { - const config = new Config( - Joi.object().keys({ - a: Joi.number().default(1), - }) - ); - - expect(config.get('a')).toBe(1); - config.removeSchema('a'); - expect(() => config.get('a')).toThrowError('Unknown config key'); - }); - - it('only removes existing keys', function () { - const config = new Config(Joi.object()); - - expect(() => config.removeSchema('b')).toThrowError('Unknown schema'); - }); - }); - }); -}); diff --git a/src/legacy/server/config/override.test.ts b/src/legacy/server/config/override.test.ts deleted file mode 100644 index d3046eb7bc8af..0000000000000 --- a/src/legacy/server/config/override.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { override } from './override'; - -describe('override(target, source)', function () { - it('should override the values form source to target', function () { - const target = { - test: { - enable: true, - host: ['something else'], - client: { - type: 'sql', - }, - }, - }; - - const source = { - test: { - host: ['host-01', 'host-02'], - client: { - type: 'nosql', - }, - foo: { - bar: { - baz: 1, - }, - }, - }, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "test": Object { - "client": Object { - "type": "nosql", - }, - "enable": true, - "foo": Object { - "bar": Object { - "baz": 1, - }, - }, - "host": Array [ - "host-01", - "host-02", - ], - }, - } - `); - }); - - it('does not mutate arguments', () => { - const target = { - foo: { - bar: 1, - baz: 1, - }, - }; - - const source = { - foo: { - bar: 2, - }, - box: 2, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "box": 2, - "foo": Object { - "bar": 2, - "baz": 1, - }, - } - `); - expect(target).not.toHaveProperty('box'); - expect(source.foo).not.toHaveProperty('baz'); - }); - - it('explodes keys with dots in them', () => { - const target = { - foo: { - bar: 1, - }, - 'baz.box.boot.bar.bar': 20, - }; - - const source = { - 'foo.bar': 2, - 'baz.box.boot': { - 'bar.foo': 10, - }, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "baz": Object { - "box": Object { - "boot": Object { - "bar": Object { - "bar": 20, - "foo": 10, - }, - }, - }, - }, - "foo": Object { - "bar": 2, - }, - } - `); - }); -}); diff --git a/src/legacy/server/config/override.ts b/src/legacy/server/config/override.ts deleted file mode 100644 index 55147c955539e..0000000000000 --- a/src/legacy/server/config/override.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const isObject = (v: any): v is Record => - typeof v === 'object' && v !== null && !Array.isArray(v); - -const assignDeep = (target: Record, source: Record) => { - for (let [key, value] of Object.entries(source)) { - // unwrap dot-separated keys - if (key.includes('.')) { - const [first, ...others] = key.split('.'); - key = first; - value = { [others.join('.')]: value }; - } - - if (isObject(value)) { - if (!target.hasOwnProperty(key)) { - target[key] = {}; - } - - assignDeep(target[key], value); - } else { - target[key] = value; - } - } -}; - -export const override = (...sources: Array>): Record => { - const result = {}; - - for (const object of sources) { - assignDeep(result, object); - } - - return result; -}; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js deleted file mode 100644 index 81fdfe04290d5..0000000000000 --- a/src/legacy/server/config/schema.js +++ /dev/null @@ -1,95 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Joi from 'joi'; -import os from 'os'; -import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; - -const HANDLED_IN_NEW_PLATFORM = Joi.any().description( - 'This key is handled in the new platform ONLY' -); -export default () => - Joi.object({ - elastic: Joi.object({ - apm: HANDLED_IN_NEW_PLATFORM, - }).default(), - - pkg: Joi.object({ - version: Joi.string().default(Joi.ref('$version')), - branch: Joi.string().default(Joi.ref('$branch')), - buildNum: Joi.number().default(Joi.ref('$buildNum')), - buildSha: Joi.string().default(Joi.ref('$buildSha')), - }).default(), - - env: Joi.object({ - name: Joi.string().default(Joi.ref('$env')), - dev: Joi.boolean().default(Joi.ref('$dev')), - prod: Joi.boolean().default(Joi.ref('$prod')), - }).default(), - - dev: HANDLED_IN_NEW_PLATFORM, - pid: HANDLED_IN_NEW_PLATFORM, - csp: HANDLED_IN_NEW_PLATFORM, - - server: Joi.object({ - name: Joi.string().default(os.hostname()), - // keep them for BWC, remove when not used in Legacy. - // validation should be in sync with one in New platform. - // https://github.com/elastic/kibana/blob/master/src/core/server/http/http_config.ts - basePath: Joi.string() - .default('') - .allow('') - .regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), - host: Joi.string().hostname().default('localhost'), - port: Joi.number().default(5601), - rewriteBasePath: Joi.boolean().when('basePath', { - is: '', - then: Joi.default(false).valid(false), - otherwise: Joi.default(false), - }), - - autoListen: HANDLED_IN_NEW_PLATFORM, - cors: HANDLED_IN_NEW_PLATFORM, - customResponseHeaders: HANDLED_IN_NEW_PLATFORM, - keepaliveTimeout: HANDLED_IN_NEW_PLATFORM, - maxPayloadBytes: HANDLED_IN_NEW_PLATFORM, - publicBaseUrl: HANDLED_IN_NEW_PLATFORM, - socketTimeout: HANDLED_IN_NEW_PLATFORM, - ssl: HANDLED_IN_NEW_PLATFORM, - compression: HANDLED_IN_NEW_PLATFORM, - uuid: HANDLED_IN_NEW_PLATFORM, - xsrf: HANDLED_IN_NEW_PLATFORM, - }).default(), - - uiSettings: HANDLED_IN_NEW_PLATFORM, - - logging: legacyLoggingConfigSchema, - - ops: Joi.object({ - interval: Joi.number().default(5000), - cGroupOverrides: HANDLED_IN_NEW_PLATFORM, - }).default(), - - plugins: HANDLED_IN_NEW_PLATFORM, - path: HANDLED_IN_NEW_PLATFORM, - stats: HANDLED_IN_NEW_PLATFORM, - status: HANDLED_IN_NEW_PLATFORM, - map: HANDLED_IN_NEW_PLATFORM, - i18n: HANDLED_IN_NEW_PLATFORM, - - // temporarily moved here from the (now deleted) kibana legacy plugin - kibana: Joi.object({ - enabled: Joi.boolean().default(true), - index: Joi.string().default('.kibana'), - autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), - // TODO Also allow units here like in elasticsearch config once this is moved to the new platform - autocompleteTimeout: Joi.number().integer().min(1).default(1000), - }).default(), - - savedObjects: HANDLED_IN_NEW_PLATFORM, - }).default(); diff --git a/src/legacy/server/config/schema.test.js b/src/legacy/server/config/schema.test.js deleted file mode 100644 index c57e6cf9a933a..0000000000000 --- a/src/legacy/server/config/schema.test.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import schemaProvider from './schema'; -import Joi from 'joi'; - -describe('Config schema', function () { - let schema; - beforeEach(async () => (schema = await schemaProvider())); - - function validate(data, options) { - return Joi.validate(data, schema, options); - } - - describe('server', function () { - it('everything is optional', function () { - const { error } = validate({}); - expect(error).toBe(null); - }); - - describe('basePath', function () { - it('accepts empty strings', function () { - const { error, value } = validate({ server: { basePath: '' } }); - expect(error).toBe(null); - expect(value.server.basePath).toBe(''); - }); - - it('accepts strings with leading slashes', function () { - const { error, value } = validate({ server: { basePath: '/path' } }); - expect(error).toBe(null); - expect(value.server.basePath).toBe('/path'); - }); - - it('rejects strings with trailing slashes', function () { - const { error } = validate({ server: { basePath: '/path/' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - }); - - it('rejects strings without leading slashes', function () { - const { error } = validate({ server: { basePath: 'path' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - }); - - it('rejects things that are not strings', function () { - for (const value of [1, true, {}, [], /foo/]) { - const { error } = validate({ server: { basePath: value } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - } - }); - }); - - describe('rewriteBasePath', function () { - it('defaults to false', () => { - const { error, value } = validate({}); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(false); - }); - - it('accepts false', function () { - const { error, value } = validate({ server: { rewriteBasePath: false } }); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(false); - }); - - it('accepts true if basePath set', function () { - const { error, value } = validate({ server: { basePath: '/foo', rewriteBasePath: true } }); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(true); - }); - - it('rejects true if basePath not set', function () { - const { error } = validate({ server: { rewriteBasePath: true } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']); - }); - - it('rejects strings', function () { - const { error } = validate({ server: { rewriteBasePath: 'foo' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']); - }); - }); - }); -}); diff --git a/src/legacy/server/core/index.ts b/src/legacy/server/core/index.ts deleted file mode 100644 index 2bdd9f26b2c22..0000000000000 --- a/src/legacy/server/core/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; -import KbnServer from '../kbn_server'; - -/** - * Exposes `kbnServer.newPlatform` through Hapi API. - * @param kbnServer KbnServer singleton instance. - * @param server Hapi server instance to expose `core` on. - */ -export function coreMixin(kbnServer: KbnServer, server: Server) { - // we suppress type error because hapi expect a function here not an object - server.decorate('server', 'newPlatform', kbnServer.newPlatform as any); -} diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js deleted file mode 100644 index 0fb51b341c3dd..0000000000000 --- a/src/legacy/server/http/index.js +++ /dev/null @@ -1,37 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { format } from 'url'; -import Boom from '@hapi/boom'; - -export default async function (kbnServer, server) { - server = kbnServer.server; - - const getBasePath = (request) => kbnServer.newPlatform.setup.core.http.basePath.get(request); - - server.route({ - method: 'GET', - path: '/{p*}', - handler: function (req, h) { - const path = req.path; - if (path === '/' || path.charAt(path.length - 1) !== '/') { - throw Boom.notFound(); - } - const basePath = getBasePath(req); - const pathPrefix = basePath ? `${basePath}/` : ''; - return h - .redirect( - format({ - search: req.url.search, - pathname: pathPrefix + path.slice(0, -1), - }) - ) - .permanent(true); - }, - }); -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts deleted file mode 100644 index 3fe0f5899668f..0000000000000 --- a/src/legacy/server/kbn_server.d.ts +++ /dev/null @@ -1,95 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; - -import { - CoreSetup, - CoreStart, - EnvironmentMode, - LoggerFactory, - PackageInfo, - LegacyServiceSetupDeps, -} from '../../core/server'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyConfig } from '../../core/server/legacy'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { UiPlugins } from '../../core/server/plugins'; - -// lot of legacy code was assuming this type only had these two methods -export type KibanaConfig = Pick; - -// Extend the defaults with the plugins and server methods we need. -declare module 'hapi' { - interface PluginProperties { - spaces: any; - } - - interface Server { - config: () => KibanaConfig; - newPlatform: KbnServer['newPlatform']; - } -} - -type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; - -export interface PluginsSetup { - [key: string]: object; -} - -export interface KibanaCore { - __internals: { - hapiServer: LegacyServiceSetupDeps['core']['http']['server']; - rendering: LegacyServiceSetupDeps['core']['rendering']; - uiPlugins: UiPlugins; - }; - env: { - mode: Readonly; - packageInfo: Readonly; - }; - setupDeps: { - core: CoreSetup; - plugins: PluginsSetup; - }; - startDeps: { - core: CoreStart; - plugins: Record; - }; - logger: LoggerFactory; -} - -export interface NewPlatform { - __internals: KibanaCore['__internals']; - env: KibanaCore['env']; - coreContext: { - logger: KibanaCore['logger']; - }; - setup: KibanaCore['setupDeps']; - start: KibanaCore['startDeps']; - stop: null; -} - -// eslint-disable-next-line import/no-default-export -export default class KbnServer { - public readonly newPlatform: NewPlatform; - public server: Server; - public inject: Server['inject']; - - constructor(settings: Record, config: KibanaConfig, core: KibanaCore); - - public ready(): Promise; - public mixin(...fns: KbnMixinFunc[]): Promise; - public listen(): Promise; - public close(): Promise; - public applyLoggingConfiguration(settings: any): void; - public config: KibanaConfig; -} - -// Re-export commonly used hapi types. -export { Server, Request, ResponseToolkit } from '@hapi/hapi'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js deleted file mode 100644 index 4bc76b6a7706f..0000000000000 --- a/src/legacy/server/kbn_server.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { constant, once, compact, flatten } from 'lodash'; -import { reconfigureLogging } from '@kbn/legacy-logging'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { fromRoot, pkg } from '../../core/server/utils'; -import { Config } from './config'; -import httpMixin from './http'; -import { coreMixin } from './core'; -import { loggingMixin } from './logging'; - -/** - * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig - * @typedef {import('./kbn_server').KibanaCore} KibanaCore - * @typedef {import('./kbn_server').LegacyPlugins} LegacyPlugins - */ - -const rootDir = fromRoot('.'); - -export default class KbnServer { - /** - * @param {Record} settings - * @param {KibanaConfig} config - * @param {KibanaCore} core - */ - constructor(settings, config, core) { - this.name = pkg.name; - this.version = pkg.version; - this.build = pkg.build || false; - this.rootDir = rootDir; - this.settings = settings || {}; - this.config = config; - - const { setupDeps, startDeps, logger, __internals, env } = core; - - this.server = __internals.hapiServer; - this.newPlatform = { - env: { - mode: env.mode, - packageInfo: env.packageInfo, - }, - __internals, - coreContext: { - logger, - }, - setup: setupDeps, - start: startDeps, - stop: null, - }; - - this.ready = constant( - this.mixin( - // Sets global HTTP behaviors - httpMixin, - - coreMixin, - - loggingMixin - ) - ); - - this.listen = once(this.listen); - } - - /** - * Extend the KbnServer outside of the constraints of a plugin. This allows access - * to APIs that are not exposed (intentionally) to the plugins and should only - * be used when the code will be kept up to date with Kibana. - * - * @param {...function} - functions that should be called to mixin functionality. - * They are called with the arguments (kibana, server, config) - * and can return a promise to delay execution of the next mixin - * @return {Promise} - promise that is resolved when the final mixin completes. - */ - async mixin(...fns) { - for (const fn of compact(flatten(fns))) { - await fn.call(this, this, this.server, this.config); - } - } - - /** - * Tell the server to listen for incoming requests, or get - * a promise that will be resolved once the server is listening. - * - * @return undefined - */ - async listen() { - await this.ready(); - - const { server } = this; - - if (process.env.isDevCliChild) { - // help parent process know when we are ready - process.send(['SERVER_LISTENING']); - } - - return server; - } - - async close() { - if (!this.server) { - return; - } - - await this.server.stop(); - } - - async inject(opts) { - if (!this.server) { - await this.ready(); - } - - return await this.server.inject(opts); - } - - applyLoggingConfiguration(settings) { - const config = Config.withDefaultSchema(settings); - - const loggingConfig = config.get('logging'); - const opsConfig = config.get('ops'); - - reconfigureLogging(this.server, loggingConfig, opsConfig.interval); - } -} diff --git a/src/legacy/server/logging/index.js b/src/legacy/server/logging/index.js deleted file mode 100644 index 1b2ae59f4aa00..0000000000000 --- a/src/legacy/server/logging/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { setupLogging, setupLoggingRotate } from '@kbn/legacy-logging'; - -export async function loggingMixin(kbnServer, server, config) { - const loggingConfig = config.get('logging'); - const opsInterval = config.get('ops.interval'); - - await setupLogging(server, loggingConfig, opsInterval); - await setupLoggingRotate(server, loggingConfig); -} diff --git a/src/legacy/utils/artifact_type.ts b/src/legacy/utils/artifact_type.ts deleted file mode 100644 index 8243b78b15025..0000000000000 --- a/src/legacy/utils/artifact_type.ts +++ /dev/null @@ -1,11 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pkg } from '../../core/server/utils'; -export const IS_KIBANA_DISTRIBUTABLE = pkg.build && pkg.build.distributable === true; -export const IS_KIBANA_RELEASE = pkg.build && pkg.build.release === true; diff --git a/src/legacy/utils/deep_clone_with_buffers.test.ts b/src/legacy/utils/deep_clone_with_buffers.test.ts deleted file mode 100644 index f23e0c8496490..0000000000000 --- a/src/legacy/utils/deep_clone_with_buffers.test.ts +++ /dev/null @@ -1,68 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { deepCloneWithBuffers } from './deep_clone_with_buffers'; - -describe('deepCloneWithBuffers()', () => { - it('deep clones objects', () => { - const source = { - a: { - b: {}, - c: {}, - d: [ - { - e: 'f', - }, - ], - }, - }; - - const output = deepCloneWithBuffers(source); - - expect(source.a).toEqual(output.a); - expect(source.a).not.toBe(output.a); - - expect(source.a.b).toEqual(output.a.b); - expect(source.a.b).not.toBe(output.a.b); - - expect(source.a.c).toEqual(output.a.c); - expect(source.a.c).not.toBe(output.a.c); - - expect(source.a.d).toEqual(output.a.d); - expect(source.a.d).not.toBe(output.a.d); - - expect(source.a.d[0]).toEqual(output.a.d[0]); - expect(source.a.d[0]).not.toBe(output.a.d[0]); - }); - - it('copies buffers but keeps them buffers', () => { - const input = Buffer.from('i am a teapot', 'utf8'); - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input)).toBe(true); - expect(Buffer.isBuffer(output)).toBe(true); - expect(Buffer.compare(output, input)); - expect(output).not.toBe(input); - }); - - it('copies buffers that are deep', () => { - const input = { - a: { - b: { - c: Buffer.from('i am a teapot', 'utf8'), - }, - }, - }; - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input.a.b.c)).toBe(true); - expect(Buffer.isBuffer(output.a.b.c)).toBe(true); - expect(Buffer.compare(output.a.b.c, input.a.b.c)); - expect(output.a.b.c).not.toBe(input.a.b.c); - }); -}); diff --git a/src/legacy/utils/deep_clone_with_buffers.ts b/src/legacy/utils/deep_clone_with_buffers.ts deleted file mode 100644 index c81a572326e7c..0000000000000 --- a/src/legacy/utils/deep_clone_with_buffers.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { cloneDeepWith } from 'lodash'; - -// We should add `any` return type to overcome bug in lodash types, customizer -// in lodash 3.* can return `undefined` if cloning is handled by the lodash, but -// type of the customizer function doesn't expect that. -function cloneBuffersCustomizer(val: unknown): any { - if (Buffer.isBuffer(val)) { - return Buffer.from(val); - } -} - -export function deepCloneWithBuffers(val: T): T { - return cloneDeepWith(val, cloneBuffersCustomizer); -} diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts deleted file mode 100644 index 92fbd6ce715a4..0000000000000 --- a/src/legacy/utils/index.d.ts +++ /dev/null @@ -1,9 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function unset(object: object, rawPath: string): void; diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js deleted file mode 100644 index a96caeb93aaa6..0000000000000 --- a/src/legacy/utils/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { deepCloneWithBuffers } from './deep_clone_with_buffers'; -export { unset } from './unset'; -export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; -export { IS_KIBANA_RELEASE } from './artifact_type'; diff --git a/src/legacy/utils/unset.js b/src/legacy/utils/unset.js deleted file mode 100644 index fa9a9cee77a13..0000000000000 --- a/src/legacy/utils/unset.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; - -export function unset(object, rawPath) { - if (!object) return; - const path = _.toPath(rawPath); - - switch (path.length) { - case 0: - return; - - case 1: - delete object[rawPath]; - break; - - default: - const leaf = path.pop(); - const parentPath = path.slice(); - const parent = _.get(object, parentPath); - unset(parent, leaf); - if (!_.size(parent)) { - unset(object, parentPath); - } - break; - } -} diff --git a/src/legacy/utils/unset.test.js b/src/legacy/utils/unset.test.js deleted file mode 100644 index 0c521ae046124..0000000000000 --- a/src/legacy/utils/unset.test.js +++ /dev/null @@ -1,90 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { unset } from './unset'; - -describe('unset(obj, key)', function () { - describe('invalid input', function () { - it('should do nothing if not given an object', function () { - const obj = 'hello'; - unset(obj, 'e'); - expect(obj).toBe('hello'); - }); - - it('should do nothing if not given a key', function () { - const obj = { one: 1 }; - unset(obj); - expect(obj).toEqual({ one: 1 }); - }); - - it('should do nothing if given an empty string as a key', function () { - const obj = { one: 1 }; - unset(obj, ''); - expect(obj).toEqual({ one: 1 }); - }); - }); - - describe('shallow removal', function () { - let obj; - - beforeEach(function () { - obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; - }); - - it('should remove the param using a string key', function () { - unset(obj, 'two'); - expect(obj).toEqual({ one: 1, deep: { three: 3, four: 4 } }); - }); - - it('should remove the param using an array key', function () { - unset(obj, ['two']); - expect(obj).toEqual({ one: 1, deep: { three: 3, four: 4 } }); - }); - }); - - describe('deep removal', function () { - let obj; - - beforeEach(function () { - obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; - }); - - it('should remove the param using a string key', function () { - unset(obj, 'deep.three'); - expect(obj).toEqual({ one: 1, two: 2, deep: { four: 4 } }); - }); - - it('should remove the param using an array key', function () { - unset(obj, ['deep', 'three']); - expect(obj).toEqual({ one: 1, two: 2, deep: { four: 4 } }); - }); - }); - - describe('recursive removal', function () { - it('should clear object if only value is removed', function () { - const obj = { one: { two: { three: 3 } } }; - unset(obj, 'one.two.three'); - expect(obj).toEqual({}); - }); - - it('should clear object if no props are left', function () { - const obj = { one: { two: { three: 3 } } }; - unset(obj, 'one.two'); - expect(obj).toEqual({}); - }); - - it('should remove deep property, then clear the object', function () { - const obj = { one: { two: { three: 3, four: 4 } } }; - unset(obj, 'one.two.three'); - expect(obj).toEqual({ one: { two: { four: 4 } } }); - - unset(obj, 'one.two.four'); - expect(obj).toEqual({}); - }); - }); -}); diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index 97a855959903d..4d62e410326b6 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json index ccb123aaec83b..aeb6837c69a99 100644 --- a/src/plugins/apm_oss/tsconfig.json +++ b/src/plugins/apm_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/bfetch/tsconfig.json b/src/plugins/bfetch/tsconfig.json index 6c01479f1929e..173ff725d07d0 100644 --- a/src/plugins/bfetch/tsconfig.json +++ b/src/plugins/bfetch/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx index ea4cf1582c7c4..9cc261bf3ed86 100644 --- a/src/plugins/charts/public/static/components/current_time.tsx +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -9,7 +9,7 @@ import moment, { Moment } from 'moment'; import React, { FC } from 'react'; -import { LineAnnotation, AnnotationDomainTypes, LineAnnotationStyle } from '@elastic/charts'; +import { LineAnnotation, AnnotationDomainType, LineAnnotationStyle } from '@elastic/charts'; import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; @@ -46,7 +46,7 @@ export const CurrentTime: FC = ({ isDarkMode, domainEnd }) => diff --git a/src/plugins/charts/tsconfig.json b/src/plugins/charts/tsconfig.json index 99edb2ffe3c16..a4f65d5937204 100644 --- a/src/plugins/charts/tsconfig.json +++ b/src/plugins/charts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index d9f49036be8f8..34aca5021bac4 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx index 49b12d46dc9a2..cda2f76930627 100644 --- a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx @@ -23,7 +23,7 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { DashboardCopyToCapabilities } from './copy_to_dashboard_action'; -import { DashboardPicker } from '../../services/presentation_util'; +import { LazyDashboardPicker, withSuspense } from '../../services/presentation_util'; import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable'; import { createDashboardEditUrl, DashboardConstants } from '../..'; @@ -37,6 +37,8 @@ interface CopyToDashboardModalProps { closeModal: () => void; } +const DashboardPicker = withSuspense(LazyDashboardPicker); + export function CopyToDashboardModal({ PresentationUtilContext, stateTransfer, diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 3d6f08f321977..e7e2ccfd46b9c 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -10,7 +10,7 @@ import { History } from 'history'; import { merge, Subject, Subscription } from 'rxjs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { debounceTime, tap } from 'rxjs/operators'; +import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; import { useKibana } from '../../../kibana_react/public'; import { DashboardConstants } from '../dashboard_constants'; import { DashboardTopNav } from './top_nav/dashboard_top_nav'; @@ -30,7 +30,7 @@ import { useSavedDashboard, } from './hooks'; -import { IndexPattern } from '../services/data'; +import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data'; import { EmbeddableRenderer } from '../services/embeddable'; import { DashboardContainerInput } from '.'; import { leaveConfirmStrings } from '../dashboard_strings'; @@ -209,14 +209,26 @@ export function DashboardApp({ ); subscriptions.add( - merge( - data.query.timefilter.timefilter.getAutoRefreshFetch$(), - searchSessionIdQuery$ - ).subscribe(() => { + searchSessionIdQuery$.subscribe(() => { triggerRefresh$.next({ force: true }); }) ); + subscriptions.add( + data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .pipe( + tap(() => { + triggerRefresh$.next({ force: true }); + }), + switchMap((done) => + // best way on a dashboard to estimate that panels are updated is to rely on search session service state + waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done)) + ) + ) + .subscribe() + ); + dashboardStateManager.registerChangeListener(() => { setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter)); // we aren't checking dirty state because there are changes the container needs to know about diff --git a/src/plugins/dashboard/public/services/presentation_util.ts b/src/plugins/dashboard/public/services/presentation_util.ts index 017b455966f13..d3e6c1ebe9eec 100644 --- a/src/plugins/dashboard/public/services/presentation_util.ts +++ b/src/plugins/dashboard/public/services/presentation_util.ts @@ -6,4 +6,8 @@ * Side Public License, v 1. */ -export { PresentationUtilPluginStart, DashboardPicker } from '../../../presentation_util/public'; +export { + PresentationUtilPluginStart, + LazyDashboardPicker, + withSuspense, +} from '../../../presentation_util/public'; diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 452208b39af60..dd99119cfb457 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 60e74a3fa126c..30006e2b497bd 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -5,7 +5,7 @@ title: Data services image: https://source.unsplash.com/400x175/?Search summary: The data plugin contains services for searching, querying and filtering. date: 2020-12-02 -tags: ['kibana','dev', 'contributor', 'api docs'] +tags: ['kibana', 'dev', 'contributor', 'api docs'] --- # data @@ -149,7 +149,6 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints: - Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}` - Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}` - ### Index Patterns API Index Patterns REST API allows you to create, retrieve and delete index patterns. I also @@ -212,11 +211,10 @@ The endpoint returns the created index pattern object. ```json { - "index_pattern": {} + "index_pattern": {} } ``` - #### Fetch an index pattern by ID Retrieve an index pattern by its ID. @@ -229,23 +227,22 @@ Returns an index pattern object. ```json { - "index_pattern": { - "id": "...", - "version": "...", - "title": "...", - "type": "...", - "intervalName": "...", - "timeFieldName": "...", - "sourceFilters": [], - "fields": {}, - "typeMeta": {}, - "fieldFormats": {}, - "fieldAttrs": {} - } + "index_pattern": { + "id": "...", + "version": "...", + "title": "...", + "type": "...", + "intervalName": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fields": {}, + "typeMeta": {}, + "fieldFormats": {}, + "fieldAttrs": {} + } } ``` - #### Delete an index pattern by ID Delete and index pattern by its ID. @@ -256,21 +253,21 @@ DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Returns an '200 OK` response with empty body on success. - #### Partially update an index pattern by ID Update part of an index pattern. Only provided fields will be updated on the index pattern, missing fields will stay as they are persisted. These fields can be update partially: - - `title` - - `timeFieldName` - - `intervalName` - - `fields` (optionally refresh fields) - - `sourceFilters` - - `fieldFormatMap` - - `type` - - `typeMeta` + +- `title` +- `timeFieldName` +- `intervalName` +- `fields` (optionally refresh fields) +- `sourceFilters` +- `fieldFormatMap` +- `type` +- `typeMeta` Update a title of an index pattern. @@ -318,18 +315,14 @@ This endpoint returns the updated index pattern object. ```json { - "index_pattern": { - - } + "index_pattern": {} } ``` - ### Fields API Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`. - #### Update fields Update endpoint allows you to update fields presentation metadata, such as `count`, @@ -383,13 +376,10 @@ This endpoint returns the updated index pattern object. ```json { - "index_pattern": { - - } + "index_pattern": {} } ``` - ### Scripted Fields API Scripted Fields API provides CRUD API for scripted fields of an index pattern. @@ -487,7 +477,7 @@ Returns the field object. ```json { - "field": {} + "field": {} } ``` @@ -529,47 +519,86 @@ POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scri } ``` - ## Query The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries. It contains sub-services for each of those configurations: - - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service. - - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings. - - `data.query.queryString` - Responsible for the query string and query language settings. - - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications. - Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes. +- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service. +- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings. +- `data.query.queryString` - Responsible for the query string and query language settings. +- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications. - A simple use case is: +Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes. - ```.ts - function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { - data.query.state$.subscribe(() => { +A simple use case is: - // Constuct the query portion of the search request - const query = data.query.getEsQuery(indexPattern); +```.ts +function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { + data.query.state$.subscribe(() => { + + // Constuct the query portion of the search request + const query = data.query.getEsQuery(indexPattern); + + // Construct a request + const request = { + params: { + index: indexPattern.title, + body: { + aggs: aggConfigs.toDsl(), + query, + }, + }, + }; + + // Search with the `data.query` config + const search$ = data.search.search(request); + + ... + }); +} - // Construct a request - const request = { - params: { - index: indexPattern.title, - body: { - aggs: aggConfigs.toDsl(), - query, - }, - }, - }; +``` - // Search with the `data.query` config - const search$ = data.search.search(request); +### Timefilter - ... - }); - } +`data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings. + +#### Autorefresh - ``` +Timefilter provides an API for setting and getting current auto refresh state: + +```ts +const { pause, value } = data.query.timefilter.timefilter.getRefreshInterval(); + +data.query.timefilter.timefilter.setRefreshInterval({ pause: false, value: 5000 }); // start auto refresh with 5 seconds interval +``` + +Timefilter API also provides an `autoRefreshFetch$` observables that apps should use to get notified +when it is time to refresh data because of auto refresh. +This API expects apps to confirm when they are done with reloading the data. +The confirmation mechanism is needed to prevent excessive queue of fetches. + +``` +import { refetchData } from '../my-app' + +const autoRefreshFetch$ = data.query.timefilter.timefilter.getAutoRefreshFetch$() +autoRefreshFetch$.subscribe((done) => { + try { + await refetchData(); + } finally { + // confirm that data fetching was finished + done(); + } +}) + +function unmount() { + // don't forget to unsubscribe when leaving the app + autoRefreshFetch$.unsubscribe() +} + +``` ## Search diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 9673d41fdbdbf..79a9e0ac5451b 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -7,6 +7,7 @@ */ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; +export const KIBANA_USER_QUERY_LANGUAGE_KEY = 'kibana.userQueryLanguage'; export const UI_SETTINGS = { META_FIELDS: 'metaFields', diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts index c46e5c5266f55..e6f815e058ce3 100644 --- a/src/plugins/data/common/kbn_field_types/types.ts +++ b/src/plugins/data/common/kbn_field_types/types.ts @@ -80,4 +80,5 @@ export enum KBN_FIELD_TYPES { OBJECT = 'object', NESTED = 'nested', HISTOGRAM = 'histogram', + MISSING = 'missing', } diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 297af560081b1..3ce528e6ed893 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -230,7 +230,7 @@ describe('AggConfigs', () => { describe('#toDsl', () => { beforeEach(() => { indexPattern = stubIndexPattern as IndexPattern; - indexPattern.fields.getByName = (name) => (name as unknown) as IndexPatternField; + indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); }); it('uses the sorted aggs', () => { diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 4e278d5872a3e..56e720d237c45 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -16,16 +16,33 @@ import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketAggConfig } from './bucket_agg_type'; import { mockAggTypesRegistry } from '../test_helpers'; +import type { IndexPatternField } from '../../../index_patterns'; +import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern'; const indexPattern = { id: '1234', title: 'logstash-*', fields: [ { - name: 'field', + name: 'machine.os.raw', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'geo.src', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, }, ], -} as any; +} as IndexPattern; + +indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); const singleTerm = { aggs: [ diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index bb34d7ede453c..09dfbb28a4e53 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -10,6 +10,8 @@ import { AggConfigs } from '../agg_configs'; import { METRIC_TYPES } from '../metrics'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; +import type { IndexPatternField } from '../../../index_patterns'; +import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern'; describe('Terms Agg', () => { describe('order agg editor UI', () => { @@ -17,16 +19,44 @@ describe('Terms Agg', () => { const indexPattern = { id: '1234', title: 'logstash-*', - fields: { - getByName: () => field, - filter: () => [field], - }, - } as any; + fields: [ + { + name: 'field', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'string_field', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'empty_number_field', + type: 'number', + esTypes: ['number'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'number_field', + type: 'number', + esTypes: ['number'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + } as IndexPattern; - const field = { - name: 'field', - indexPattern, - }; + indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); + indexPattern.fields.filter = () => indexPattern.fields; return new AggConfigs( indexPattern, @@ -207,16 +237,28 @@ describe('Terms Agg', () => { const indexPattern = { id: '1234', title: 'logstash-*', - fields: { - getByName: () => field, - filter: () => [field], - }, - } as any; + fields: [ + { + name: 'string_field', + type: 'string', + esTypes: ['string'], + aggregatable: true, + filterable: true, + searchable: true, + }, + { + name: 'number_field', + type: 'number', + esTypes: ['number'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + } as IndexPattern; - const field = { - name: 'field', - indexPattern, - }; + indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField); + indexPattern.fields.filter = () => indexPattern.fields; const aggConfigs = new AggConfigs( indexPattern, diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index 2d3ff8f5fdba8..62dac9831211a 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -8,7 +8,10 @@ import { i18n } from '@kbn/i18n'; import { IAggConfig } from '../agg_config'; -import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/common'; +import { + SavedFieldNotFound, + SavedFieldTypeInvalidForAgg, +} from '../../../../../../plugins/kibana_utils/common'; import { BaseParamType } from './base'; import { propFilter } from '../utils'; import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types'; @@ -47,13 +50,49 @@ export class FieldParamType extends BaseParamType { ); } - if (field.scripted) { + if (field.type === KBN_FIELD_TYPES.MISSING) { + throw new SavedFieldNotFound( + i18n.translate( + 'data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage', + { + defaultMessage: + 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.', + values: { + fieldParameter: field.name, + }, + } + ) + ); + } + + const validField = this.getAvailableFields(aggConfig).find( + (f: any) => f.name === field.name + ); + + if (!validField) { + throw new SavedFieldTypeInvalidForAgg( + i18n.translate( + 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', + { + defaultMessage: + 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.', + values: { + fieldParameter: field.name, + aggType: aggConfig?.type?.title, + indexPatternTitle: aggConfig.getIndexPattern().title, + }, + } + ) + ); + } + + if (validField.scripted) { output.params.script = { - source: field.script, - lang: field.lang, + source: validField.script, + lang: validField.lang, }; } else { - output.params.field = field.name; + output.params.field = validField.name; } }; } @@ -69,28 +108,15 @@ export class FieldParamType extends BaseParamType { const field = aggConfig.getIndexPattern().fields.getByName(fieldName); if (!field) { - throw new SavedObjectNotFound('index-pattern-field', fieldName); - } - - const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName); - if (!validField) { - throw new Error( - i18n.translate( - 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', - { - defaultMessage: - 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.', - values: { - fieldParameter: fieldName, - aggType: aggConfig?.type?.title, - indexPatternTitle: aggConfig.getIndexPattern().title, - }, - } - ) - ); + return new IndexPatternField({ + type: KBN_FIELD_TYPES.MISSING, + name: fieldName, + searchable: false, + aggregatable: false, + }); } - return validField; + return field; }; } diff --git a/src/plugins/data/common/search/expressions/exists_filter.test.ts b/src/plugins/data/common/search/expressions/exists_filter.test.ts index 60e8a9c7a09ce..e3b53b2281398 100644 --- a/src/plugins/data/common/search/expressions/exists_filter.test.ts +++ b/src/plugins/data/common/search/expressions/exists_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { existsFilterFunction } from './exists_filter'; diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts index 56a9e1ce660cd..ac8ae55492cc0 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { kibanaFilterFunction } from './kibana_filter'; diff --git a/src/plugins/data/common/search/expressions/phrase_filter.test.ts b/src/plugins/data/common/search/expressions/phrase_filter.test.ts index 90e471e166f5e..39bd907513a0d 100644 --- a/src/plugins/data/common/search/expressions/phrase_filter.test.ts +++ b/src/plugins/data/common/search/expressions/phrase_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { phraseFilterFunction } from './phrase_filter'; diff --git a/src/plugins/data/common/search/expressions/range_filter.test.ts b/src/plugins/data/common/search/expressions/range_filter.test.ts index 129e6bd82e16a..92670f8a044ba 100644 --- a/src/plugins/data/common/search/expressions/range_filter.test.ts +++ b/src/plugins/data/common/search/expressions/range_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { rangeFilterFunction } from './range_filter'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c47cd6cd9740d..d2683e248b7bf 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -388,6 +388,8 @@ export { PainlessError, noSearchSessionStorageCapabilityMessage, SEARCH_SESSIONS_MANAGEMENT_ID, + waitUntilNextSessionCompletes$, + WaitUntilNextSessionCompletesOptions, } from './search'; export type { @@ -467,6 +469,7 @@ export { TimeHistoryContract, QueryStateChange, QueryStart, + AutoRefreshDoneFn, } from './query'; export { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts index fa33f00a49879..36569cafd6611 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -9,6 +9,7 @@ import sinon from 'sinon'; import { CoreSetup } from 'src/core/public'; +import { SerializedFieldFormat } from 'src/plugins/expressions/public'; import { IFieldType, FieldSpec } from '../../common/index_patterns'; import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../'; import { getFieldFormatsRegistry } from '../test_utils'; @@ -51,6 +52,7 @@ export class StubIndexPattern { _reindexFields: Function; stubSetFieldFormat: Function; fields?: FieldSpec[]; + setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; constructor( pattern: string, @@ -74,6 +76,10 @@ export class StubIndexPattern { this.metaFields = ['_id', '_type', '_source']; this.fieldFormatMap = {}; + this.setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { + this.fieldFormatMap[fieldName] = format; + }; + this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); this.flattenHit = indexPatterns.flattenHitWrapper( (this as unknown) as IndexPattern, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7f243cefd08b6..05925f097de24 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -504,6 +504,11 @@ export interface ApplyGlobalFilterActionContext { // @public (undocumented) export type AutocompleteStart = ReturnType; +// Warning: (ae-missing-release-tag) "AutoRefreshDoneFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AutoRefreshDoneFn = () => void; + // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1781,6 +1786,8 @@ export enum KBN_FIELD_TYPES { // (undocumented) IP_RANGE = "ip_range", // (undocumented) + MISSING = "missing", + // (undocumented) MURMUR3 = "murmur3", // (undocumented) NESTED = "nested", @@ -2022,6 +2029,8 @@ export const QueryStringInput: (props: QueryStringInputProps) => JSX.Element; // // @public (undocumented) export interface QueryStringInputProps { + // (undocumented) + autoSubmit?: boolean; // (undocumented) bubbleSubmitEvent?: boolean; // (undocumented) @@ -2071,6 +2080,8 @@ export interface QueryStringInputProps { // (undocumented) size?: SuggestionsListSize; // (undocumented) + storageKey?: string; + // (undocumented) submitOnBlur?: boolean; } @@ -2643,6 +2654,18 @@ export const UI_SETTINGS: { readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange"; }; +// Warning: (ae-missing-release-tag) "waitUntilNextSessionCompletes$" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable; + +// Warning: (ae-missing-release-tag) "WaitUntilNextSessionCompletesOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface WaitUntilNextSessionCompletesOptions { + waitForIdle?: number; +} + // Warnings were encountered during analysis: // @@ -2690,21 +2713,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index fbf89af2abec4..0fdbb8965a5e5 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -11,7 +11,7 @@ import { skip } from 'rxjs/operators'; import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { Query, UI_SETTINGS } from '../../../common'; +import { KIBANA_USER_QUERY_LANGUAGE_KEY, Query, UI_SETTINGS } from '../../../common'; export class QueryStringManager { private query$: BehaviorSubject; @@ -25,7 +25,7 @@ export class QueryStringManager { private getDefaultLanguage() { return ( - this.storage.get('kibana.userQueryLanguage') || + this.storage.get(KIBANA_USER_QUERY_LANGUAGE_KEY) || this.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) ); } diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index 83e897824d86c..3dfd4e0fe514f 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -9,7 +9,7 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service'; export * from './types'; -export { Timefilter, TimefilterContract } from './timefilter'; +export { Timefilter, TimefilterContract, AutoRefreshDoneFn } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter'; export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts new file mode 100644 index 0000000000000..3c8b316c3b878 --- /dev/null +++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createAutoRefreshLoop, AutoRefreshDoneFn } from './auto_refresh_loop'; + +jest.useFakeTimers(); + +test('triggers refresh with interval', () => { + const { loop$, start, stop } = createAutoRefreshLoop(); + + const fn = jest.fn((done) => done()); + loop$.subscribe(fn); + + jest.advanceTimersByTime(5000); + expect(fn).not.toBeCalled(); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(2); + + stop(); + + jest.advanceTimersByTime(5000); + expect(fn).toHaveBeenCalledTimes(2); +}); + +test('waits for done() to be called', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done!: AutoRefreshDoneFn; + const fn = jest.fn((_done) => { + done = _done; + }); + loop$.subscribe(fn); + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(1); + expect(done).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn).toHaveBeenCalledTimes(1); + + done(); + + jest.advanceTimersByTime(500); + expect(fn).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn).toHaveBeenCalledTimes(2); +}); + +test('waits for done() from multiple subscribers to be called', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + done1 = _done; + }); + loop$.subscribe(fn1); + + let done2!: AutoRefreshDoneFn; + const fn2 = jest.fn((_done) => { + done2 = _done; + }); + loop$.subscribe(fn2); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); + + done2(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); +}); + +test('unsubscribe() resets the state', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + done1 = _done; + }); + loop$.subscribe(fn1); + + const fn2 = jest.fn(); + const sub2 = loop$.subscribe(fn2); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); + + sub2.unsubscribe(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); +}); + +test('calling done() twice is ignored', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + done1 = _done; + }); + loop$.subscribe(fn1); + + const fn2 = jest.fn(); + loop$.subscribe(fn2); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(1); +}); + +test('calling older done() is ignored', () => { + const { loop$, start } = createAutoRefreshLoop(); + + let done1!: AutoRefreshDoneFn; + const fn1 = jest.fn((_done) => { + // @ts-ignore + if (done1) return; + done1 = _done; + }); + loop$.subscribe(fn1); + + start(1000); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + expect(done1).toBeInstanceOf(Function); + + jest.advanceTimersByTime(1001); + expect(fn1).toHaveBeenCalledTimes(1); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); + + done1(); + + jest.advanceTimersByTime(500); + expect(fn1).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(501); + expect(fn1).toHaveBeenCalledTimes(2); +}); diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts new file mode 100644 index 0000000000000..1e213b36e1d8b --- /dev/null +++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { defer, Subject } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; +import { once } from 'lodash'; + +export type AutoRefreshDoneFn = () => void; + +/** + * Creates a loop for timepicker's auto refresh + * It has a "confirmation" mechanism: + * When auto refresh loop emits, it won't continue automatically, + * until each subscriber calls received `done` function. + * + * @internal + */ +export const createAutoRefreshLoop = () => { + let subscribersCount = 0; + const tick = new Subject(); + + let _timeoutHandle: number; + let _timeout: number = 0; + + function start() { + stop(); + if (_timeout === 0) return; + const timeoutHandle = window.setTimeout(() => { + let pendingDoneCount = subscribersCount; + const done = () => { + if (timeoutHandle !== _timeoutHandle) return; + + pendingDoneCount--; + if (pendingDoneCount === 0) { + start(); + } + }; + tick.next(done); + }, _timeout); + + _timeoutHandle = timeoutHandle; + } + + function stop() { + window.clearTimeout(_timeoutHandle); + _timeoutHandle = -1; + } + + return { + stop: () => { + _timeout = 0; + stop(); + }, + start: (timeout: number) => { + _timeout = timeout; + if (subscribersCount > 0) { + start(); + } + }, + loop$: defer(() => { + subscribersCount++; + start(); // restart the loop on a new subscriber + return tick.pipe(map((doneCb) => once(doneCb))); // each subscriber allowed to call done only once + }).pipe( + finalize(() => { + subscribersCount--; + if (subscribersCount === 0) { + stop(); + } else { + start(); // restart the loop to potentially unblock the interval + } + }) + ), + }; +}; diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts index 8e1e76ed19e6d..92ee6b0c30428 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.test.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts @@ -10,7 +10,7 @@ jest.useFakeTimers(); import sinon from 'sinon'; import moment from 'moment'; -import { Timefilter } from './timefilter'; +import { AutoRefreshDoneFn, Timefilter } from './timefilter'; import { Subscription } from 'rxjs'; import { TimeRange, RefreshInterval } from '../../../common'; import { createNowProviderMock } from '../../now_provider/mocks'; @@ -121,7 +121,7 @@ describe('setRefreshInterval', () => { beforeEach(() => { update = sinon.spy(); fetch = sinon.spy(); - autoRefreshFetch = sinon.spy(); + autoRefreshFetch = sinon.spy((done) => done()); timefilter.setRefreshInterval({ pause: false, value: 0, @@ -344,3 +344,44 @@ describe('calculateBounds', () => { expect(() => timefilter.calculateBounds(timeRange)).toThrowError(); }); }); + +describe('getAutoRefreshFetch$', () => { + test('next auto refresh loop starts after "done" called', () => { + const autoRefreshFetch = jest.fn(); + let doneCb: AutoRefreshDoneFn | undefined; + timefilter.getAutoRefreshFetch$().subscribe((done) => { + autoRefreshFetch(); + doneCb = done; + }); + timefilter.setRefreshInterval({ pause: false, value: 1000 }); + + expect(autoRefreshFetch).toBeCalledTimes(0); + jest.advanceTimersByTime(5000); + expect(autoRefreshFetch).toBeCalledTimes(1); + + if (doneCb) doneCb(); + + jest.advanceTimersByTime(1005); + expect(autoRefreshFetch).toBeCalledTimes(2); + }); + + test('new getAutoRefreshFetch$ subscription restarts refresh loop', () => { + const autoRefreshFetch = jest.fn(); + const fetch$ = timefilter.getAutoRefreshFetch$(); + const sub1 = fetch$.subscribe((done) => { + autoRefreshFetch(); + // this done will be never called, but loop will be reset by another subscription + }); + timefilter.setRefreshInterval({ pause: false, value: 1000 }); + + expect(autoRefreshFetch).toBeCalledTimes(0); + jest.advanceTimersByTime(5000); + expect(autoRefreshFetch).toBeCalledTimes(1); + + fetch$.subscribe(autoRefreshFetch); + expect(autoRefreshFetch).toBeCalledTimes(1); + sub1.unsubscribe(); + jest.advanceTimersByTime(1005); + expect(autoRefreshFetch).toBeCalledTimes(2); + }); +}); diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 436b18f70a2f8..9894010601d2b 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -22,6 +22,9 @@ import { TimeRange, } from '../../../common'; import { TimeHistoryContract } from './time_history'; +import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop'; + +export { AutoRefreshDoneFn }; // TODO: remove! @@ -32,8 +35,6 @@ export class Timefilter { private timeUpdate$ = new Subject(); // Fired when a user changes the the autorefresh settings private refreshIntervalUpdate$ = new Subject(); - // Used when an auto refresh is triggered - private autoRefreshFetch$ = new Subject(); private fetch$ = new Subject(); private _time: TimeRange; @@ -45,11 +46,12 @@ export class Timefilter { private _isTimeRangeSelectorEnabled: boolean = false; private _isAutoRefreshSelectorEnabled: boolean = false; - private _autoRefreshIntervalId: number = 0; - private readonly timeDefaults: TimeRange; private readonly refreshIntervalDefaults: RefreshInterval; + // Used when an auto refresh is triggered + private readonly autoRefreshLoop = createAutoRefreshLoop(); + constructor( config: TimefilterConfig, timeHistory: TimeHistoryContract, @@ -86,9 +88,13 @@ export class Timefilter { return this.refreshIntervalUpdate$.asObservable(); }; - public getAutoRefreshFetch$ = () => { - return this.autoRefreshFetch$.asObservable(); - }; + /** + * Get an observable that emits when it is time to refetch data due to refresh interval + * Each subscription to this observable resets internal interval + * Emitted value is a callback {@link AutoRefreshDoneFn} that must be called to restart refresh interval loop + * Apps should use this callback to start next auto refresh loop when view finished updating + */ + public getAutoRefreshFetch$ = () => this.autoRefreshLoop.loop$; public getFetch$ = () => { return this.fetch$.asObservable(); @@ -166,13 +172,9 @@ export class Timefilter { } } - // Clear the previous auto refresh interval and start a new one (if not paused) - clearInterval(this._autoRefreshIntervalId); - if (!newRefreshInterval.pause) { - this._autoRefreshIntervalId = window.setInterval( - () => this.autoRefreshFetch$.next(), - newRefreshInterval.value - ); + this.autoRefreshLoop.stop(); + if (!newRefreshInterval.pause && newRefreshInterval.value !== 0) { + this.autoRefreshLoop.start(newRefreshInterval.value); } }; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 0f2b01f618186..c22f62f45a709 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -20,7 +20,7 @@ const createSetupContractMock = () => { getEnabledUpdated$: jest.fn(), getTimeUpdate$: jest.fn(), getRefreshIntervalUpdate$: jest.fn(), - getAutoRefreshFetch$: jest.fn(() => new Observable()), + getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()), getFetch$: jest.fn(), getTime: jest.fn(), setTime: jest.fn(), diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index fded4c46992c0..92a5c36202e6f 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -45,6 +45,8 @@ export { ISessionsClient, noSearchSessionStorageCapabilityMessage, SEARCH_SESSIONS_MANAGEMENT_ID, + waitUntilNextSessionCompletes$, + WaitUntilNextSessionCompletesOptions, } from './session'; export { getEsPreference } from './es_search'; diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts index 15410400a33e6..ce578378a2fe8 100644 --- a/src/plugins/data/public/search/session/index.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -11,3 +11,7 @@ export { SearchSessionState } from './search_session_state'; export { SessionsClient, ISessionsClient } from './sessions_client'; export { noSearchSessionStorageCapabilityMessage } from './i18n'; export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants'; +export { + waitUntilNextSessionCompletes$, + WaitUntilNextSessionCompletesOptions, +} from './session_helpers'; diff --git a/src/plugins/data/public/search/session/session_helpers.test.ts b/src/plugins/data/public/search/session/session_helpers.test.ts new file mode 100644 index 0000000000000..5b64e7b554d18 --- /dev/null +++ b/src/plugins/data/public/search/session/session_helpers.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { waitUntilNextSessionCompletes$ } from './session_helpers'; +import { ISessionService, SessionService } from './session_service'; +import { BehaviorSubject } from 'rxjs'; +import { SearchSessionState } from './search_session_state'; +import { NowProviderInternalContract } from '../../now_provider'; +import { coreMock } from '../../../../../core/public/mocks'; +import { createNowProviderMock } from '../../now_provider/mocks'; +import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants'; +import { getSessionsClientMock } from './mocks'; + +let sessionService: ISessionService; +let state$: BehaviorSubject; +let nowProvider: jest.Mocked; +let currentAppId$: BehaviorSubject; + +beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext(); + const startService = coreMock.createSetup().getStartServices; + nowProvider = createNowProviderMock(); + currentAppId$ = new BehaviorSubject('app'); + sessionService = new SessionService( + initializerContext, + () => + startService().then(([coreStart, ...rest]) => [ + { + ...coreStart, + application: { + ...coreStart.application, + currentAppId$, + capabilities: { + ...coreStart.application.capabilities, + management: { + kibana: { + [SEARCH_SESSIONS_MANAGEMENT_ID]: true, + }, + }, + }, + }, + }, + ...rest, + ]), + getSessionsClientMock(), + nowProvider, + { freezeState: false } // needed to use mocks inside state container + ); + state$ = new BehaviorSubject(SearchSessionState.None); + sessionService.state$.subscribe(state$); +}); + +describe('waitUntilNextSessionCompletes$', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + test('emits when next session starts', () => { + sessionService.start(); + let untrackSearch = sessionService.trackSearch({ abort: () => {} }); + untrackSearch(); + + const next = jest.fn(); + const complete = jest.fn(); + waitUntilNextSessionCompletes$(sessionService).subscribe({ next, complete }); + expect(next).not.toBeCalled(); + + sessionService.start(); + expect(next).not.toBeCalled(); + + untrackSearch = sessionService.trackSearch({ abort: () => {} }); + untrackSearch(); + + expect(next).not.toBeCalled(); + jest.advanceTimersByTime(500); + expect(next).not.toBeCalled(); + jest.advanceTimersByTime(1000); + expect(next).toBeCalledTimes(1); + expect(complete).toBeCalled(); + }); +}); diff --git a/src/plugins/data/public/search/session/session_helpers.ts b/src/plugins/data/public/search/session/session_helpers.ts new file mode 100644 index 0000000000000..1f0a2da7e93f4 --- /dev/null +++ b/src/plugins/data/public/search/session/session_helpers.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { debounceTime, first, skipUntil } from 'rxjs/operators'; +import { ISessionService } from './session_service'; +import { SearchSessionState } from './search_session_state'; + +/** + * Options for {@link waitUntilNextSessionCompletes$} + */ +export interface WaitUntilNextSessionCompletesOptions { + /** + * For how long to wait between session state transitions before considering that session completed + */ + waitForIdle?: number; +} + +/** + * Creates an observable that emits when next search session completes. + * This utility is helpful to use in the application to delay some tasks until next session completes. + * + * @param sessionService - {@link ISessionService} + * @param opts - {@link WaitUntilNextSessionCompletesOptions} + */ +export function waitUntilNextSessionCompletes$( + sessionService: ISessionService, + { waitForIdle = 1000 }: WaitUntilNextSessionCompletesOptions = { waitForIdle: 1000 } +) { + return sessionService.state$.pipe( + // wait until new session starts + skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.None))), + // wait until new session starts loading + skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.Loading))), + // debounce to ignore quick switches from loading <-> completed. + // that could happen between sequential search requests inside a single session + debounceTime(waitForIdle), + // then wait until it finishes + first( + (state) => + state === SearchSessionState.Completed || state === SearchSessionState.BackgroundCompleted + ) + ); +} diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 466cc8c3de0b7..4e12f11668734 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -17,6 +17,16 @@ @include kbnThemeStyle('v8') { background-color: $euiFormBackgroundColor; + border-radius: $euiFormControlBorderRadius; + + &.kbnQueryBar__textareaWrap--hasPrepend { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + &.kbnQueryBar__textareaWrap--hasAppend { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } } @@ -35,8 +45,16 @@ } @include kbnThemeStyle('v8') { - border-radius: 0; padding-bottom: $euiSizeS + 1px; + + &.kbnQueryBar__textarea--hasPrepend { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + &.kbnQueryBar__textarea--hasAppend { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } &:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) { diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 16e1325b2b56b..0f660f87266fd 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -37,6 +37,7 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; import { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '..'; +import { KIBANA_USER_QUERY_LANGUAGE_KEY } from '../../../common'; export interface QueryStringInputProps { indexPatterns: Array; @@ -67,6 +68,14 @@ export interface QueryStringInputProps { */ nonKqlMode?: 'lucene' | 'text'; nonKqlModeHelpText?: string; + /** + * @param autoSubmit if user selects a value, in that case kuery will be auto submitted + */ + autoSubmit?: boolean; + /** + * @param storageKey this key is used to use user preference between kql and non-kql mode + */ + storageKey?: string; } interface Props extends QueryStringInputProps { @@ -99,6 +108,10 @@ const KEY_CODES = { // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default class QueryStringInputUI extends Component { + static defaultProps = { + storageKey: KIBANA_USER_QUERY_LANGUAGE_KEY, + }; + public state: State = { isSuggestionsVisible: false, index: null, @@ -218,7 +231,7 @@ export default class QueryStringInputUI extends Component { const recentSearches = this.persistedLog.get(); const matchingRecentSearches = recentSearches.filter((recentQuery) => { const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery; - return recentQueryString.includes(query); + return recentQueryString !== '' && recentQueryString.includes(query); }); return matchingRecentSearches.map((recentSearch) => { const text = toUser(recentSearch); @@ -393,8 +406,13 @@ export default class QueryStringInputUI extends Component { selectionStart: start + (cursorIndex ? cursorIndex : text.length), selectionEnd: start + (cursorIndex ? cursorIndex : text.length), }); + const isTypeRecentSearch = type === QuerySuggestionTypes.RecentSearch; + + const isAutoSubmitAndValid = + this.props.autoSubmit && + (type === QuerySuggestionTypes.Value || [':*', ': *'].includes(value.trim())); - if (type === QuerySuggestionTypes.RecentSearch) { + if (isTypeRecentSearch || isAutoSubmitAndValid) { this.setState({ isSuggestionsVisible: false, index: null }); this.onSubmit({ query: newQueryString, language: this.props.query.language }); } @@ -488,12 +506,16 @@ export default class QueryStringInputUI extends Component { body: JSON.stringify({ opt_in: language === 'kuery' }), }); - this.services.storage.set('kibana.userQueryLanguage', language); + const storageKey = this.props.storageKey; + this.services.storage.set(storageKey!, language); const newQuery = { query: '', language }; this.onChange(newQuery); this.onSubmit(newQuery); - this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`); + this.reportUiCounter?.( + METRIC_TYPE.LOADED, + storageKey ? `${storageKey}:language:${language}` : `query_string:language:${language}` + ); }; private onOutsideClick = () => { @@ -660,7 +682,14 @@ export default class QueryStringInputUI extends Component { ); const inputClassName = classNames( 'kbnQueryBar__textarea', - this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null + this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null, + this.props.prepend ? 'kbnQueryBar__textarea--hasPrepend' : null, + !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textarea--hasAppend' : null + ); + const inputWrapClassName = classNames( + 'euiFormControlLayout__childrenWrapper kbnQueryBar__textareaWrap', + this.props.prepend ? 'kbnQueryBar__textareaWrap--hasPrepend' : null, + !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textareaWrap--hasAppend' : null ); return ( @@ -689,7 +718,7 @@ export default class QueryStringInputUI extends Component { >
{ })} onClick={() => { this.onQueryStringChange(''); + if (this.props.autoSubmit) { + this.onSubmit({ query: '', language: this.props.query.language }); + } }} > diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 29a5a67239171..053b60956fa92 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -966,7 +966,7 @@ export class IndexPatternsServiceProvider implements Plugin_3, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1082,6 +1082,8 @@ export enum KBN_FIELD_TYPES { // (undocumented) IP_RANGE = "ip_range", // (undocumented) + MISSING = "missing", + // (undocumented) MURMUR3 = "murmur3", // (undocumented) NESTED = "nested", @@ -1232,7 +1234,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index b99a2f6f85904..9c95878af631e 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json index f369396b17fbe..c17b2341fd42f 100644 --- a/src/plugins/dev_tools/tsconfig.json +++ b/src/plugins/dev_tools/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 2c80fc111c740..3be047859d3b0 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import { merge, Subject, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; +import { debounceTime, tap, filter } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; @@ -393,12 +393,11 @@ function discoverController($route, $scope) { $scope.state.index = $scope.indexPattern.id; $scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern); - $scope.opts.fetch = $scope.fetch = function () { + $scope.opts.fetch = $scope.fetch = async function () { $scope.fetchCounter++; $scope.fetchError = undefined; if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { $scope.resultState = 'none'; - return; } // Abort any in-progress requests before fetching again @@ -494,11 +493,19 @@ function discoverController($route, $scope) { showUnmappedFields, }; + // handler emitted by `timefilter.getAutoRefreshFetch$()` + // to notify when data completed loading and to start a new autorefresh loop + let autoRefreshDoneCb; const fetch$ = merge( refetch$, filterManager.getFetches$(), timefilter.getFetch$(), - timefilter.getAutoRefreshFetch$(), + timefilter.getAutoRefreshFetch$().pipe( + tap((done) => { + autoRefreshDoneCb = done; + }), + filter(() => $scope.fetchStatus !== fetchStatuses.LOADING) + ), data.query.queryString.getUpdates$(), searchSessionManager.newSearchSessionIdFromURL$ ).pipe(debounceTime(100)); @@ -508,7 +515,16 @@ function discoverController($route, $scope) { $scope, fetch$, { - next: $scope.fetch, + next: async () => { + try { + await $scope.fetch(); + } finally { + // if there is a saved `autoRefreshDoneCb`, notify auto refresh service that + // the last fetch is completed so it starts the next auto refresh loop if needed + autoRefreshDoneCb?.(); + autoRefreshDoneCb = undefined; + } + }, }, (error) => addFatalError(core.fatalErrors, error) ) diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap index f3c8990388024..f976b961d8520 100644 --- a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap @@ -543,6 +543,7 @@ exports[`discover sidebar field details footer renders properly 1`] = ` "_source", ], "popularizeField": [Function], + "setFieldFormat": [Function], "stubSetFieldFormat": [Function], "timeFieldName": "time", "title": "logstash-*", diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 96765d76a340b..ec98199c3423e 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index e8418970d83f7..a0cd213b7bf24 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -9,7 +9,7 @@ import { cloneDeep, isEqual } from 'lodash'; import * as Rx from 'rxjs'; import { merge } from 'rxjs'; -import { debounceTime, distinctUntilChanged, map, mapTo, skip } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators'; import { RenderCompleteDispatcher } from '../../../../kibana_utils/public'; import { Adapters } from '../types'; import { IContainer } from '../containers'; @@ -111,10 +111,9 @@ export abstract class Embeddable< * In case corresponding state change triggered `reload` this stream is guarantied to emit later, * which allows to skip any state handling in case `reload` already handled it. */ - public getUpdated$(): Readonly> { + public getUpdated$(): Readonly> { return merge(this.getInput$().pipe(skip(1)), this.getOutput$().pipe(skip(1))).pipe( - debounceTime(0), - mapTo(undefined) + debounceTime(0) ); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index b9719542adc81..3f0907acabdfa 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -282,7 +282,7 @@ export abstract class Embeddable>; + getUpdated$(): Readonly>; // (undocumented) readonly id: string; // (undocumented) diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json index eacfa831ecee5..27a887500fb68 100644 --- a/src/plugins/embeddable/tsconfig.json +++ b/src/plugins/embeddable/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index a98a74375638d..42e18b72057ce 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -36,7 +36,7 @@ export const handleEsError = ({ return response.customError({ statusCode, body: { - message: body.error?.reason, + message: body.error?.reason ?? error.message ?? 'Unknown error', attributes: { // The full original ES error object error: body.error, diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json index 3d102daaf3aaf..9bcda2e0614de 100644 --- a/src/plugins/es_ui_shared/tsconfig.json +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/expressions/common/mocks.ts b/src/plugins/expressions/common/mocks.ts index 20bdbca07f008..eaeebd8e53492 100644 --- a/src/plugins/expressions/common/mocks.ts +++ b/src/plugins/expressions/common/mocks.ts @@ -34,5 +34,3 @@ export const createMockExecutionContext = ...extraContext, }; }; - -export { createMockContext } from './util/test_utils'; diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index 5f83d962d5aea..470dfc3c2d436 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -10,3 +10,4 @@ export * from './create_error'; export * from './get_by_alias'; export * from './tables_adapter'; export * from './expressions_inspector_adapter'; +export * from './test_utils'; diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 65925b5a2e4c2..4165b8906a20e 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -118,12 +118,15 @@ export class ExpressionLoader { return this.execution ? (this.execution.inspect() as Adapters) : undefined; } - update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void { + async update( + expression?: string | ExpressionAstExpression, + params?: IExpressionLoaderParams + ): Promise { this.setParams(params); this.loadingSubject.next(true); if (expression) { - this.loadData(expression, this.params); + await this.loadData(expression, this.params); } else if (this.data) { this.render(this.data); } diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index fe76ba3050a3b..cce71013cefa5 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index 19ab5a8e6efec..b15e1fc011b92 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx index 73a4837d6e0cc..69092b2bc0922 100644 --- a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx @@ -64,14 +64,14 @@ const geti18nTexts = (fieldsToDelete?: string[]) => { typeConfirm: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.typeConfirm', { - defaultMessage: "Type 'REMOVE' to confirm", + defaultMessage: 'Enter REMOVE to confirm', } ), warningRemovingFields: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningRemovingFields', { defaultMessage: - 'Warning: Removing fields may break searches or visualizations that rely on this field.', + 'Removing fields can break searches and visualizations that rely on this field.', } ), }; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx index 562f15301590b..7d79200bc6f87 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import React, { useState, useMemo } from 'react'; import { act } from 'react-dom/test-utils'; import '../../test_utils/setup_environment'; import { registerTestBed, TestBed, getCommonActions } from '../../test_utils'; +import { RuntimeFieldPainlessError } from '../../lib'; import { Field } from '../../types'; import { FieldEditor, Props, FieldEditorFormState } from './field_editor'; @@ -208,5 +210,66 @@ describe('', () => { expect(getLastStateUpdate().isValid).toBe(true); expect(form.getErrorsMessages()).toEqual([]); }); + + test('should clear the painless syntax error whenever the field type changes', async () => { + const field: Field = { + name: 'myRuntimeField', + type: 'keyword', + script: { source: 'emit(6)' }, + }; + + const TestComponent = () => { + const dummyError = { + reason: 'Awwww! Painless syntax error', + message: '', + position: { offset: 0, start: 0, end: 0 }, + scriptStack: [''], + }; + const [error, setError] = useState(null); + const clearError = useMemo(() => () => setError(null), []); + const syntaxError = useMemo(() => ({ error, clear: clearError }), [error, clearError]); + + return ( + <> + + + {/* Button to forward dummy syntax error */} + + + ); + }; + + const customTestbed = registerTestBed(TestComponent, { + memoryRouter: { + wrapComponent: false, + }, + })() as TestBed; + + testBed = { + ...customTestbed, + actions: getCommonActions(customTestbed), + }; + + const { + form, + component, + find, + actions: { changeFieldType }, + } = testBed; + + // We set some dummy painless error + act(() => { + find('setPainlessErrorButton').simulate('click'); + }); + component.update(); + + expect(form.getErrorsMessages()).toEqual(['Awwww! Painless syntax error']); + + // We change the type and expect the form error to not be there anymore + await changeFieldType('long'); + expect(form.getErrorsMessages()).toEqual([]); + }); }); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx index afb87bd1e7334..3785096e20627 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -21,6 +21,7 @@ import type { CoreStart } from 'src/core/public'; import { Form, useForm, + useFormData, FormHook, UseField, TextField, @@ -184,6 +185,9 @@ const FieldEditorComponent = ({ serializer: formSerializer, }); const { submit, isValid: isFormValid, isSubmitted } = form; + const { clear: clearSyntaxError } = syntaxError; + + const [{ type }] = useFormData({ form }); const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); const i18nTexts = geti18nTexts(); @@ -194,6 +198,12 @@ const FieldEditorComponent = ({ } }, [onChange, isFormValid, isSubmitted, submit]); + useEffect(() => { + // Whenever the field "type" changes we clear any possible painless syntax + // error as it is possibly stale. + clearSyntaxError(); + }, [type, clearSyntaxError]); + return (
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts index 46414c264c6b7..286931ad0e854 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -140,7 +140,7 @@ describe('', () => { find, component, form, - actions: { toggleFormRow }, + actions: { toggleFormRow, changeFieldType }, } = setup({ ...defaultProps, onSave }); act(() => { @@ -173,14 +173,7 @@ describe('', () => { }); // Change the type and make sure it is forwarded - act(() => { - find('typeField').simulate('change', [ - { - label: 'Other type', - value: 'other_type', - }, - ]); - }); + await changeFieldType('other_type', 'Other type'); await act(async () => { find('fieldSaveButton').simulate('click'); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index 486df1a7707af..13830f9233b5e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -53,20 +53,29 @@ const geti18nTexts = (field?: Field) => { confirmButtonText: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel', { - defaultMessage: 'Save', + defaultMessage: 'Save changes', } ), warningChangingFields: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields', { defaultMessage: - 'Warning: Changing name or type may break searches or visualizations that rely on this field.', + 'Changing name or type can break searches and visualizations that rely on this field.', } ), typeConfirm: i18n.translate( 'indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm', { - defaultMessage: "Type 'CHANGE' to continue:", + defaultMessage: 'Enter CHANGE to continue', + } + ), + titleConfirmChanges: i18n.translate( + 'indexPatternFieldEditor.saveRuntimeField.confirmModal.title', + { + defaultMessage: `Save changes to '{name}'`, + values: { + name: field?.name, + }, } ), }; @@ -134,7 +143,7 @@ const FieldEditorFlyoutContentComponent = ({ const [isValidating, setIsValidating] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); - const [confirmContent, setConfirmContent] = useState(); + const [confirmContent, setConfirmContent] = useState(''); const { submit, isValid: isFormValid, isSubmitted } = formState; const { fields } = indexPattern; @@ -211,7 +220,7 @@ const FieldEditorFlyoutContentComponent = ({ const modal = isModalVisible ? ( { @@ -21,7 +22,20 @@ export const getCommonActions = (testBed: TestBed) => { testBed.form.toggleEuiSwitch(testSubj); }; + const changeFieldType = async (value: string, label?: string) => { + await act(async () => { + testBed.find('typeField').simulate('change', [ + { + value, + label: label ?? value, + }, + ]); + }); + testBed.component.update(); + }; + return { toggleFormRow, + changeFieldType, }; }; diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json index c638fd34c6bbb..559b1aaf0fc26 100644 --- a/src/plugins/index_pattern_field_editor/tsconfig.json +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 3c8fdb1cf6597..37bd3e4aa5bbb 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json index c2f8d8783e822..bef7bc394a6cc 100644 --- a/src/plugins/input_control_vis/tsconfig.json +++ b/src/plugins/input_control_vis/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/inspector/tsconfig.json b/src/plugins/inspector/tsconfig.json index 0e42e577428c6..2a9c41464532c 100644 --- a/src/plugins/inspector/tsconfig.json +++ b/src/plugins/inspector/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_legacy/tsconfig.json b/src/plugins/kibana_legacy/tsconfig.json index 0b3f42cd3b57b..709036c9e82f4 100644 --- a/src/plugins/kibana_legacy/tsconfig.json +++ b/src/plugins/kibana_legacy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index 3396861cb9179..ac3ac109cb35f 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_react/tsconfig.json b/src/plugins/kibana_react/tsconfig.json index 857b8cf83645c..eb9a24ca141f6 100644 --- a/src/plugins/kibana_react/tsconfig.json +++ b/src/plugins/kibana_react/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index dc91181268be7..fcdd00380755f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -412,6 +412,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInspectEsQueries': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -428,7 +432,7 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInspectEsQueries': { + 'labs:presentation:unifiedToolbar': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 810f13931225f..613ada418c6e7 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -118,4 +118,5 @@ export interface UsageStats { 'banners:placement': string; 'banners:textColor': string; 'banners:backgroundColor': string; + 'labs:presentation:unifiedToolbar': boolean; } diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index 100f1f03955d0..ee07dfe589e4a 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/*", diff --git a/src/plugins/kibana_utils/common/errors/errors.ts b/src/plugins/kibana_utils/common/errors/errors.ts index 7a9495cc8f413..7f3efc6d9571f 100644 --- a/src/plugins/kibana_utils/common/errors/errors.ts +++ b/src/plugins/kibana_utils/common/errors/errors.ts @@ -32,7 +32,7 @@ export class DuplicateField extends KbnError { export class SavedObjectNotFound extends KbnError { public savedObjectType: string; public savedObjectId?: string; - constructor(type: string, id?: string, link?: string) { + constructor(type: string, id?: string, link?: string, customMessage?: string) { const idMsg = id ? ` (id: ${id})` : ''; let message = `Could not locate that ${type}${idMsg}`; @@ -40,13 +40,31 @@ export class SavedObjectNotFound extends KbnError { message += `, [click here to re-create it](${link})`; } - super(message); + super(customMessage || message); this.savedObjectType = type; this.savedObjectId = id; } } +/** + * A saved field doesn't exist anymore + */ +export class SavedFieldNotFound extends KbnError { + constructor(message: string) { + super(message); + } +} + +/** + * A saved field type isn't compatible with aggregation + */ +export class SavedFieldTypeInvalidForAgg extends KbnError { + constructor(message: string) { + super(message); + } +} + /** * This error is for scenarios where a saved object is detected that has invalid JSON properties. * There was a scenario where we were importing objects with double-encoded JSON, and the system diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json index d9572707e8662..ae5e9b90af807 100644 --- a/src/plugins/kibana_utils/tsconfig.json +++ b/src/plugins/kibana_utils/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json index d6689ea1067db..ec006d492499e 100644 --- a/src/plugins/legacy_export/tsconfig.json +++ b/src/plugins/legacy_export/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index 3423299a53df7..ba3661666631a 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.test.js b/src/plugins/maps_ems/public/service_settings/service_settings.test.js index 5bd371aace79b..eb67997c253b9 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.test.js +++ b/src/plugins/maps_ems/public/service_settings/service_settings.test.js @@ -103,43 +103,8 @@ describe('service_settings (FKA tile_map test)', function () { expect(tmsService.attribution.includes('OpenStreetMap')).toEqual(true); }); - describe('modify - url', function () { - let tilemapServices; - + describe('tms mods', function () { let serviceSettings; - async function assertQuery(expected) { - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - const urlObject = url.parse(attrs.url, true); - Object.keys(expected).forEach((key) => { - expect(urlObject.query[key]).toEqual(expected[key]); - }); - } - - it('accepts an object', async () => { - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('merged additions with previous values', async () => { - // ensure that changes are always additive - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar', bar: 'stool' }); - }); - - it('overwrites conflicting previous values', async () => { - serviceSettings = makeServiceSettings(); - // ensure that conflicts are overwritten - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - serviceSettings.setQueryParams({ foo: 'tstool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'tstool', bar: 'stool' }); - }); it('should merge in tilemap url', async () => { serviceSettings = makeServiceSettings( @@ -161,7 +126,7 @@ describe('service_settings (FKA tile_map test)', function () { id: 'road_map', name: 'Road Map - Bright', url: - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl', minZoom: 0, maxZoom: 10, attribution: @@ -208,19 +173,19 @@ describe('service_settings (FKA tile_map test)', function () { ); expect(desaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationFalse.maxZoom).toEqual(10); expect(desaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationTrue.maxZoom).toEqual(18); expect(darkThemeDesaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationFalse.maxZoom).toEqual(22); expect(darkThemeDesaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationTrue.maxZoom).toEqual(22); }); @@ -264,14 +229,13 @@ describe('service_settings (FKA tile_map test)', function () { describe('File layers', function () { it('should load manifest (all props)', async function () { const serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); const urlObject = url.parse(fileUrl, true); - Object.keys({ foo: 'bar', elastic_tile_service_tos: 'agree' }).forEach((key) => { + Object.keys({ elastic_tile_service_tos: 'agree' }).forEach((key) => { expect(typeof urlObject.query[key]).toEqual('string'); }); }); diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.ts b/src/plugins/maps_ems/public/service_settings/service_settings.ts index f7c735b6c3037..412db42a1570c 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings.ts @@ -22,7 +22,6 @@ export class ServiceSettings implements IServiceSettings { private readonly _mapConfig: MapsEmsConfig; private readonly _tilemapsConfig: TileMapConfig; private readonly _hasTmsConfigured: boolean; - private _showZoomMessage: boolean; private readonly _emsClient: EMSClient; private readonly tmsOptionsFromConfig: any; @@ -31,7 +30,6 @@ export class ServiceSettings implements IServiceSettings { this._tilemapsConfig = tilemapsConfig; this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; - this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), appVersion: getKibanaVersion(), @@ -45,6 +43,9 @@ export class ServiceSettings implements IServiceSettings { return fetch(...args); }, }); + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this._emsClient.addQueryParams({ license: 'sspl' }); const markdownIt = new MarkdownIt({ html: false, @@ -58,18 +59,6 @@ export class ServiceSettings implements IServiceSettings { }); } - shouldShowZoomMessage({ origin }: { origin: string }): boolean { - return origin === ORIGIN.EMS && this._showZoomMessage; - } - - enableZoomMessage(): void { - this._showZoomMessage = true; - } - - disableZoomMessage(): void { - this._showZoomMessage = false; - } - __debugStubManifestCalls(manifestRetrieval: () => Promise): { removeStub: () => void } { const oldGetManifest = this._emsClient.getManifest; diff --git a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts index 80a9aae835844..6b04bd200eba8 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts @@ -46,8 +46,6 @@ export interface IServiceSettings { getFileLayers(): Promise; getUrlForRegionLayer(layer: FileLayer): Promise; setQueryParams(params: { [p: string]: string }): void; - enableZoomMessage(): void; - disableZoomMessage(): void; getAttributesForTMSLayer( tmsServiceConfig: TmsLayer, isDesaturated: boolean, diff --git a/src/plugins/maps_ems/tsconfig.json b/src/plugins/maps_ems/tsconfig.json index 7f44da00d47a4..b85c3da66b83a 100644 --- a/src/plugins/maps_ems/tsconfig.json +++ b/src/plugins/maps_ems/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index 8e283288e34b2..f321274791a3b 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,5 @@ "ui": true, "server": true, "requiredPlugins": ["mapsEms"], - "requiredBundles": ["kibanaReact", "visDefaultEditor", "mapsEms"] + "requiredBundles": ["visDefaultEditor", "mapsEms"] } diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 9cd574c5246e8..a261bcf6edd80 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -193,13 +193,12 @@ export function BaseMapsVisualizationProvider() { isDesaturated, isDarkMode ); - const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._kibanaMap.setBaseLayer({ baseLayerType: 'tms', - options: { ...options, showZoomMessage, ...meta }, + options: { ...options, ...meta }, }); } diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index eea8315419289..62dbbda2588a5 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -7,13 +7,11 @@ */ import { EventEmitter } from 'events'; -import { createZoomWarningMsg } from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../maps_ems/common'; -import { getToasts } from '../kibana_services'; import { L } from '../leaflet'; function makeFitControl(fitContainer, kibanaMap) { @@ -479,22 +477,6 @@ export class KibanaMap extends EventEmitter { this._updateLegend(); } - _addMaxZoomMessage = (layer) => { - const zoomWarningMsg = createZoomWarningMsg( - getToasts(), - this.getZoomLevel, - this.getMaxZoomLevel - ); - - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - }; - setLegendPosition(position) { if (this._legendPosition === position) { if (!this._leafletLegendControl) { @@ -572,11 +554,6 @@ export class KibanaMap extends EventEmitter { }); this._leafletBaseLayer = baseLayer; - if (settings.options.showZoomMessage) { - baseLayer.on('add', () => { - this._addMaxZoomMessage(baseLayer); - }); - } this._leafletBaseLayer.addTo(this._leafletMap); this._leafletBaseLayer.bringToBack(); if (settings.options.minZoom > this._leafletMap.getZoom()) { diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js deleted file mode 100644 index f60d819f0b390..0000000000000 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; -import { toMountPoint } from '../../../kibana_react/public'; - -export const createZoomWarningMsg = (function () { - let disableZoomMsg = false; - const setZoomMsg = (boolDisableMsg) => (disableZoomMsg = boolDisableMsg); - - class ZoomWarning extends React.Component { - constructor(props) { - super(props); - this.state = { - disabled: false, - }; - } - - render() { - return ( -
-

- - {`default distribution `} - - ), - ems: ( - - {`Elastic Maps Service`} - - ), - wms: ( - - {`Custom WMS Configuration`} - - ), - configSettings: ( - - {`Custom TMS Using Config Settings`} - - ), - }} - /> -

- - { - this.setState( - { - disabled: true, - }, - () => this.props.onChange(this.state.disabled) - ); - }} - data-test-subj="suppressZoomWarnings" - > - {`Don't show again`} - -
- ); - } - } - - const zoomToast = { - title: 'No additional zoom levels', - text: toMountPoint(), - 'data-test-subj': 'maxZoomWarning', - }; - - return (toastService, getZoomLevel, getMaxZoomLevel) => { - return () => { - const zoomLevel = getZoomLevel(); - const maxMapZoom = getMaxZoomLevel(); - if (!disableZoomMsg && zoomLevel === maxMapZoom) { - toastService.addDanger(zoomToast); - } - }; - }; -})(); diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json index c600024cc4a74..f757e35f785af 100644 --- a/src/plugins/maps_legacy/tsconfig.json +++ b/src/plugins/maps_legacy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json index bb86142e1c443..07cfe10d7d81f 100644 --- a/src/plugins/navigation/tsconfig.json +++ b/src/plugins/navigation/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 84626b2f3a6a8..66244a22336c7 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 8b556af07dd62..bf8819b13a92d 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -8,3 +8,5 @@ export const PLUGIN_ID = 'presentationUtil'; export const PLUGIN_NAME = 'presentationUtil'; + +export * from './labs'; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts new file mode 100644 index 0000000000000..65e42996ae910 --- /dev/null +++ b/src/plugins/presentation_util/common/labs.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIFIED_TOOLBAR = 'labs:presentation:unifiedToolbar'; + +export const projectIDs = [UNIFIED_TOOLBAR] as const; +export const environmentNames = ['kibana', 'browser', 'session'] as const; +export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; + +/** + * This is a list of active Labs Projects for the Presentation Team. It is the "source of truth" for all projects + * provided to users of our solutions in Kibana. + */ +export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { + [UNIFIED_TOOLBAR]: { + id: UNIFIED_TOOLBAR, + isActive: false, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectName', { + defaultMessage: 'Unified Toolbar', + }), + description: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectDescription', { + defaultMessage: 'Enable the new unified toolbar design for Presentation solutions', + }), + solutions: ['dashboard', 'canvas'], + }, +}; + +export type ProjectID = typeof projectIDs[number]; +export type EnvironmentName = typeof environmentNames[number]; +export type SolutionName = typeof solutionNames[number]; + +export type EnvironmentStatus = { + [env in EnvironmentName]?: boolean; +}; + +export type ProjectStatus = { + defaultValue: boolean; + isEnabled: boolean; + isOverride: boolean; +} & EnvironmentStatus; + +export interface ProjectConfig { + id: ProjectID; + name: string; + isActive: boolean; + environments: EnvironmentName[]; + description: string; + solutions: SolutionName[]; +} + +export type Project = ProjectConfig & { status: ProjectStatus }; + +export const getProjectIDs = () => projectIDs; + +export const isProjectEnabledByStatus = (active: boolean, status: EnvironmentStatus): boolean => { + // If the project is enabled by default, then any false flag will flip the switch, and vice-versa. + return active + ? Object.values(status).every((value) => value === true) + : Object.values(status).some((value) => value === true); +}; diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index b1b3d768c3e76..c7d272dcd02a1 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -2,8 +2,10 @@ "id": "presentationUtil", "version": "1.0.0", "kibanaVersion": "kibana", - "server": false, + "server": true, "ui": true, - "requiredPlugins": ["savedObjects"], + "requiredPlugins": [ + "savedObjects" + ], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index d32afca5cedeb..47ba570765028 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -99,3 +99,7 @@ export function DashboardPicker(props: DashboardPickerProps) { /> ); } + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default DashboardPicker; diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx new file mode 100644 index 0000000000000..af806e1c22f1a --- /dev/null +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Suspense, ComponentType, ReactElement } from 'react'; +import { EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui'; + +/** + * A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors. + * @param Component A component deferred by `React.lazy` + * @param fallback A fallback component to render while things load; default is `EuiLoadingSpinner` + */ +export const withSuspense =

( + Component: ComponentType

, + fallback: ReactElement | null = +) => (props: P) => ( + + + + + +); + +export const LazyLabsBeakerButton = withSuspense( + React.lazy(() => import('./labs/labs_beaker_button')) +); + +export const LazyLabsFlyout = withSuspense(React.lazy(() => import('./labs/labs_flyout'))); + +export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); + +export const LazySavedObjectSaveModalDashboard = React.lazy( + () => import('./saved_object_save_modal_dashboard') +); diff --git a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx new file mode 100644 index 0000000000000..0acdd433cbac8 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/environment_switch.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiIconTip, + EuiSpacer, + EuiScreenReaderOnly, +} from '@elastic/eui'; + +import { EnvironmentName } from '../../../common/labs'; +import { LabsStrings } from '../../i18n'; + +const { Switch: strings } = LabsStrings.Components; + +const switchText: { [env in EnvironmentName]: { name: string; help: string } } = { + kibana: strings.getKibanaSwitchText(), + browser: strings.getBrowserSwitchText(), + session: strings.getSessionSwitchText(), +}; + +export interface Props { + env: EnvironmentName; + isChecked: boolean; + onChange: (checked: boolean) => void; + name: string; +} + +export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => ( + + + + + + {name} - + + {switchText[env].name} + + } + onChange={(e) => onChange(e.target.checked)} + compressed + /> + + + + + + + +); diff --git a/src/plugins/presentation_util/public/components/labs/labs.stories.tsx b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx new file mode 100644 index 0000000000000..a9a1a0753d24b --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { LabsBeakerButton } from './labs_beaker_button'; +import { LabsFlyout } from './labs_flyout'; + +export default { + title: 'Labs/Flyout', + description: + 'A set of components used for providing Labs controls and projects in another solution.', + argTypes: {}, +}; + +export function BeakerButton() { + return ; +} + +export function Flyout() { + return ; +} + +export function EmptyFlyout() { + return ; +} diff --git a/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx new file mode 100644 index 0000000000000..6d7fd4afdac68 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiIcon, EuiNotificationBadge, EuiButtonProps } from '@elastic/eui'; + +import { pluginServices } from '../../services'; +import { LabsFlyout, Props as FlyoutProps } from './labs_flyout'; + +export type Props = EuiButtonProps & Pick; + +export const LabsBeakerButton = ({ solutions, ...props }: Props) => { + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects } = labsService.useService(); + const [isOpen, setIsOpen] = useState(false); + + const projects = getProjects(); + + const [overrideCount, onEnabledCountChange] = useState( + Object.values(projects).filter((project) => project.status.isOverride).length + ); + + const onButtonClick = () => setIsOpen((open) => !open); + const onClose = () => setIsOpen(false); + + return ( + <> + + + {overrideCount > 0 ? ( + + {overrideCount} + + ) : null} + + {isOpen ? : null} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsBeakerButton; diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx new file mode 100644 index 0000000000000..562d3b291a4b3 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactNode, useRef, useState, useEffect } from 'react'; +import { + EuiFlyout, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiButton, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiIcon, +} from '@elastic/eui'; + +import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; +import { pluginServices } from '../../services'; +import { LabsStrings } from '../../i18n'; + +import { ProjectList } from './project_list'; + +const { Flyout: strings } = LabsStrings.Components; + +export interface Props { + onClose: () => void; + solutions?: SolutionName[]; + onEnabledCountChange?: (overrideCount: number) => void; +} + +const hasStatusChanged = ( + original: Record, + current: Record +): boolean => { + for (const id of Object.keys(original) as ProjectID[]) { + for (const key of Object.keys(original[id].status) as Array) { + if (original[id].status[key] !== current[id].status[key]) { + return true; + } + } + } + return false; +}; + +export const getOverridenCount = (projects: Record) => + Object.values(projects).filter((project) => project.status.isOverride).length; + +export const LabsFlyout = (props: Props) => { + const { solutions, onEnabledCountChange = () => {}, onClose } = props; + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects, setProjectStatus, reset } = labsService.useService(); + + const [projects, setProjects] = useState(getProjects()); + const [overrideCount, setOverrideCount] = useState(getOverridenCount(projects)); + const initialStatus = useRef(getProjects()); + + const isChanged = hasStatusChanged(initialStatus.current, projects); + + useEffect(() => { + setOverrideCount(getOverridenCount(projects)); + }, [projects]); + + useEffect(() => { + onEnabledCountChange(overrideCount); + }, [onEnabledCountChange, overrideCount]); + + const onStatusChange = (id: ProjectID, env: EnvironmentName, enabled: boolean) => { + setProjectStatus(id, env, enabled); + setProjects(getProjects()); + }; + + let footer: ReactNode = null; + + const resetButton = ( + { + reset(); + setProjects(getProjects()); + }} + isDisabled={!overrideCount} + > + {strings.getResetToDefaultLabel()} + + ); + + const refreshButton = ( + { + window.location.reload(); + }} + isDisabled={!isChanged} + > + {strings.getRefreshLabel()} + + ); + + footer = ( + + + {resetButton} + {refreshButton} + + + ); + + return ( + + + +

+ + + + + {strings.getTitleLabel()} + +

+ + + + + + {footer} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsFlyout; diff --git a/src/plugins/presentation_util/public/components/labs/project_list.tsx b/src/plugins/presentation_util/public/components/labs/project_list.tsx new file mode 100644 index 0000000000000..4ecf45409b02c --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiCallOut } from '@elastic/eui'; + +import { SolutionName, ProjectID, Project } from '../../../common'; +import { ProjectListItem, Props as ProjectListItemProps } from './project_list_item'; + +import { LabsStrings } from '../../i18n'; + +const { List: strings } = LabsStrings.Components; + +export interface Props { + solutions?: SolutionName[]; + projects: Record; + onStatusChange: ProjectListItemProps['onStatusChange']; +} + +const EmptyList = () => ; + +export const ProjectList = (props: Props) => { + const { solutions, projects, onStatusChange } = props; + + const items = Object.values(projects) + .map((project) => { + // Filter out any panels that don't match the solutions filter, (if provided). + if (solutions && !solutions.some((solution) => project.solutions.includes(solution))) { + return null; + } + + return ( +
  • + +
  • + ); + }) + .filter((item) => item !== null); + + return ( + + {items.length > 0 ?
      {items}
    : } +
    + ); +}; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.scss b/src/plugins/presentation_util/public/components/labs/project_list_item.scss new file mode 100644 index 0000000000000..c91a07576b314 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.scss @@ -0,0 +1,46 @@ +.projectListItem { + position: relative; + background: $euiColorEmptyShade; + padding: $euiSizeL; + min-width: 500px; + + &--isOverridden:before { + position: absolute; + top: $euiSizeL; + left: 4px; + bottom: $euiSizeL; + width: 4px; + background: $euiColorPrimary; + content: ''; + } + + .euiSwitch__label { + width: 100%; + } +} + +.projectListItem + .projectListItem:after { + position: absolute; + top: 0; + right: 0; + left: 0; + height: 1px; + background: $euiColorLightShade; + content: ''; +} + +.euiFlyout .projectListItem { + padding: $euiSizeL $euiSizeXS; + + &:first-child { + padding-top: 0; + } + + &--isOverridden:before { + left: -12px; + } + + &--isOverridden:first-child:before { + top: 0; + } +} diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx new file mode 100644 index 0000000000000..ce93abded521e --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { mapValues } from 'lodash'; + +import { EnvironmentStatus, ProjectConfig, ProjectID, ProjectStatus } from '../../../common'; +import { applyProjectStatus } from '../../services/labs'; +import { ProjectListItem, Props } from './project_list_item'; + +import { projects as projectConfigs } from '../../../common'; +import { ProjectList } from './project_list'; + +export default { + title: 'Labs/ProjectList', + description: 'A set of controls for displaying and manipulating projects.', +}; + +const projects = mapValues(projectConfigs, (project) => + applyProjectStatus(project, { kibana: false, session: false, browser: false }) +); + +export function List() { + return ; +} + +export function EmptyList() { + return ; +} + +export const ListItem = ( + props: Pick< + Props['project'], + 'description' | 'isActive' | 'name' | 'solutions' | 'environments' + > & + Omit +) => { + const { kibana, browser, session, ...rest } = props; + const status: EnvironmentStatus = { kibana, browser, session }; + const projectConfig: ProjectConfig = { + ...rest, + id: 'storybook:component' as ProjectID, + }; + + return ( +
    + ({ ...status, [env]: enabled })} + /> +
    + ); +}; + +ListItem.args = { + isActive: false, + name: 'Demo Project', + description: 'This is a demo project, and this is the description of the demo project.', + kibana: false, + browser: false, + session: false, + solutions: ['dashboard', 'canvas'], + environments: ['kibana', 'browser', 'session'], +}; + +ListItem.argTypes = { + environments: { + control: { + type: 'check', + options: ['kibana', 'browser', 'session'], + }, + }, +}; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx new file mode 100644 index 0000000000000..e4aa1abd3693c --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiTitle, + EuiText, + EuiFormFieldset, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import classnames from 'classnames'; + +import { ProjectID, EnvironmentName, Project, environmentNames } from '../../../common/labs'; +import { EnvironmentSwitch } from './environment_switch'; + +import { LabsStrings } from '../../i18n'; +const { ListItem: strings } = LabsStrings.Components; + +import './project_list_item.scss'; + +export interface Props { + project: Project; + onStatusChange: (id: ProjectID, env: EnvironmentName, enabled: boolean) => void; +} + +export const ProjectListItem = ({ project, onStatusChange }: Props) => { + const { id, status, isActive, name, description, solutions } = project; + const { isEnabled, isOverride } = status; + + return ( + + + + + + +

    {name}

    +
    +
    + +
    + {solutions.map((solution) => ( + {solution} + ))} +
    +
    + + {description} + + + + {isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()} + + +
    +
    + + + + {name} + + {strings.getOverrideLegend()} + + ), + }} + > + {environmentNames.map((env) => { + const envStatus = status[env]; + if (envStatus !== undefined) { + return ( + onStatusChange(id, env, checked)} + {...{ env, name }} + /> + ); + } + })} + + +
    +
    + ); +}; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 4491be04b1a42..6c36cf8b8e3a7 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -10,32 +10,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - OnSaveProps, - SaveModalState, - SavedObjectSaveModal, -} from '../../../../plugins/saved_objects/public'; +import { OnSaveProps, SavedObjectSaveModal } from '../../../../plugins/saved_objects/public'; -import './saved_object_save_modal_dashboard.scss'; import { pluginServices } from '../services'; +import { SaveModalDashboardProps } from './types'; import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; -interface SaveModalDocumentInfo { - id?: string; - title: string; - description?: string; -} - -export interface SaveModalDashboardProps { - documentInfo: SaveModalDocumentInfo; - canSaveByReference: boolean; - objectType: string; - onClose: () => void; - onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; - tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); -} +import './saved_object_save_modal_dashboard.scss'; -export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { +function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); @@ -136,3 +119,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { /> ); } + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default SavedObjectSaveModalDashboard; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 78a1569c02ead..53aaecb070c7a 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -22,7 +22,7 @@ import { EuiCheckbox, } from '@elastic/eui'; -import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; +import DashboardPicker, { DashboardPickerProps } from './dashboard_picker'; import './saved_object_save_modal_dashboard.scss'; diff --git a/src/plugins/presentation_util/public/components/types.ts b/src/plugins/presentation_util/public/components/types.ts new file mode 100644 index 0000000000000..7c5c50982f49e --- /dev/null +++ b/src/plugins/presentation_util/public/components/types.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { OnSaveProps, SaveModalState } from '../../../../plugins/saved_objects/public'; + +interface SaveModalDocumentInfo { + id?: string; + title: string; + description?: string; +} + +export interface SaveModalDashboardProps { + documentInfo: SaveModalDocumentInfo; + canSaveByReference: boolean; + objectType: string; + onClose: () => void; + onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; + tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); +} diff --git a/src/legacy/server/config/index.js b/src/plugins/presentation_util/public/i18n/index.ts similarity index 91% rename from src/legacy/server/config/index.js rename to src/plugins/presentation_util/public/i18n/index.ts index 6fb77eb2a3777..cf2f2c111ad58 100644 --- a/src/legacy/server/config/index.js +++ b/src/plugins/presentation_util/public/i18n/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { Config } from './config'; +export * from './labs'; diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx new file mode 100644 index 0000000000000..ddf6346bd68ca --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const LabsStrings = { + Components: { + Switch: { + getKibanaSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.kibanaSwitchName', { + defaultMessage: 'Kibana', + }), + help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { + defaultMessage: 'Sets the corresponding Advanced Setting for this lab project in Kibana', + }), + }), + getBrowserSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.browserSwitchName', { + defaultMessage: 'Browser', + }), + help: i18n.translate('presentationUtil.labs.components.browserSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for the browser; persists between browser instances', + }), + }), + getSessionSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.sessionSwitchName', { + defaultMessage: 'Session', + }), + help: i18n.translate('presentationUtil.labs.components.sessionSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for this tab; resets when the browser tab is closed', + }), + }), + }, + List: { + getNoProjectsMessage: () => + i18n.translate('presentationUtil.labs.components.noProjectsMessage', { + defaultMessage: 'No available lab projects', + }), + }, + ListItem: { + getOverrideLegend: () => + i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { + defaultMessage: 'Override flags', + }), + getEnabledStatusMessage: () => ( + Enabled, + }} + description="Displays the current status of a lab project" + /> + ), + getDisabledStatusMessage: () => ( + Disabled, + }} + description="Displays the current status of a lab project" + /> + ), + }, + Flyout: { + getTitleLabel: () => + i18n.translate('presentationUtil.labs.components.titleLabel', { + defaultMessage: 'Lab projects', + }), + getResetToDefaultLabel: () => + i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { + defaultMessage: 'Reset to defaults', + }), + getLabFlagsLabel: () => + i18n.translate('presentationUtil.labs.components.labFlagsLabel', { + defaultMessage: 'Lab flags', + }), + getRefreshLabel: () => + i18n.translate('presentationUtil.labs.components.calloutHelp', { + defaultMessage: 'Refresh to apply changes', + }), + }, + }, +}; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index f13807032db3e..1cbf4b5a4f334 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -8,14 +8,18 @@ import { PresentationUtilPlugin } from './plugin'; -export { - SavedObjectSaveModalDashboard, - SaveModalDashboardProps, -} from './components/saved_object_save_modal_dashboard'; +export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; +export { SaveModalDashboardProps } from './components/types'; +export { projectIDs, ProjectID, Project } from '../common/labs'; -export { DashboardPicker } from './components/dashboard_picker'; +export { + LazyLabsBeakerButton, + LazyLabsFlyout, + LazyDashboardPicker, + LazySavedObjectSaveModalDashboard, + withSuspense, +} from './components'; export function plugin() { return new PresentationUtilPlugin(); } -export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 6f74198bb56ab..00931c5730fe3 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -36,9 +36,9 @@ export class PresentationUtilPlugin startPlugins: PresentationUtilPluginStartDeps ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - return { ContextProvider: pluginServices.getContextProvider(), + labsService: pluginServices.getServices().labs, }; } diff --git a/src/core/server/utils/from_root.ts b/src/plugins/presentation_util/public/services/capabilities.ts similarity index 67% rename from src/core/server/utils/from_root.ts rename to src/plugins/presentation_util/public/services/capabilities.ts index 377f4d0e29ca5..58d56d1a4d81d 100644 --- a/src/core/server/utils/from_root.ts +++ b/src/plugins/presentation_util/public/services/capabilities.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { resolve } from 'path'; -import { pkg } from './package_json'; - -export function fromRoot(...args: string[]) { - return resolve(pkg.__dirname, ...args); +export interface PresentationCapabilitiesService { + canAccessDashboards: () => boolean; + canCreateNewDashboards: () => boolean; + canSaveVisualizations: () => boolean; } diff --git a/src/plugins/presentation_util/public/services/create/index.ts b/src/plugins/presentation_util/public/services/create/index.ts index 66f7185913323..163e25e26babf 100644 --- a/src/plugins/presentation_util/public/services/create/index.ts +++ b/src/plugins/presentation_util/public/services/create/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { mapValues } from 'lodash'; - import { PluginServiceRegistry } from './registry'; export { PluginServiceRegistry } from './registry'; @@ -18,6 +16,8 @@ export { KibanaPluginServiceParams, } from './factory'; +type ServiceHooks = { [K in keyof Services]: { useService: () => Services[K] } }; + /** * `PluginServices` is a top-level class for specifying and accessing services within a plugin. * @@ -70,13 +70,27 @@ export class PluginServices { /** * Return a map of React Hooks that can be used in React components. */ - getHooks(): { [K in keyof Services]: { useService: () => Services[K] } } { + getHooks(): ServiceHooks { const registry = this.getRegistry(); const providers = registry.getServiceProviders(); - // @ts-expect-error Need to fix this; the type isn't fully understood when inferred. - return mapValues(providers, (provider) => ({ - useService: provider.getUseServiceHook(), - })); + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = { useService: providers[providerName].getServiceReactHook() }; + return acc; + }, {} as ServiceHooks); + } + + getServices(): Services { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = providers[providerName].getService(); + return acc; + }, {} as Services); } } diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index fa16e291a656d..06590bcfbb3d0 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -41,9 +41,9 @@ export class PluginServiceProvider { } /** - * Private getter that will enforce proper setup throughout the class. + * Getter that will enforce proper setup throughout the class. */ - private getService() { + public getService() { if (!this.pluginService) { throw new Error('Service not started'); } @@ -62,7 +62,7 @@ export class PluginServiceProvider { /** * Returns a function for providing a Context hook for the service. */ - getUseServiceHook() { + getServiceReactHook() { return () => { const service = useContext(this.context); diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index 61ada16e241a5..e8f85666bcac4 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { values } from 'lodash'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; /** @@ -47,16 +46,17 @@ export class PluginServiceRegistry { * Returns a React Context Provider for use in consuming applications. */ getContextProvider() { + const values = Object.values(this.getServiceProviders()) as Array< + PluginServiceProvider + >; + // Collect and combine Context.Provider elements from each Service Provider into a single // Functional Component. const provider: React.FC = ({ children }) => ( <> - {values>(this.getServiceProviders()).reduceRight( - (acc, serviceProvider) => { - return {acc}; - }, - children - )} + {values.reduceRight((acc, serviceProvider) => { + return {acc}; + }, children)} ); @@ -69,9 +69,8 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - values>(this.providers).map((serviceProvider) => - serviceProvider.start(params) - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].start(params)); this._isStarted = true; return this; } @@ -80,9 +79,8 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - values>(this.providers).map((serviceProvider) => - serviceProvider.stop() - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].stop()); this._isStarted = false; return this; } diff --git a/src/plugins/presentation_util/public/services/dashboards.ts b/src/plugins/presentation_util/public/services/dashboards.ts new file mode 100644 index 0000000000000..cbca79223063b --- /dev/null +++ b/src/plugins/presentation_util/public/services/dashboards.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SimpleSavedObject } from 'src/core/public'; +import { PartialDashboardAttributes } from './kibana/dashboards'; + +export interface PresentationDashboardsService { + findDashboards: ( + query: string, + fields: string[] + ) => Promise>>; + findDashboardsByTitle: ( + title: string + ) => Promise>>; +} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 39dae92aa2ba9..c01a95f64619c 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -6,29 +6,14 @@ * Side Public License, v 1. */ -import { SimpleSavedObject } from 'src/core/public'; import { PluginServices } from './create'; -import { PartialDashboardAttributes } from './kibana/dashboards'; - -export interface PresentationDashboardsService { - findDashboards: ( - query: string, - fields: string[] - ) => Promise>>; - findDashboardsByTitle: ( - title: string - ) => Promise>>; -} - -export interface PresentationCapabilitiesService { - canAccessDashboards: () => boolean; - canCreateNewDashboards: () => boolean; - canSaveVisualizations: () => boolean; -} - +import { PresentationCapabilitiesService } from './capabilities'; +import { PresentationDashboardsService } from './dashboards'; +import { PresentationLabsService } from './labs'; export interface PresentationUtilServices { dashboards: PresentationDashboardsService; capabilities: PresentationCapabilitiesService; + labs: PresentationLabsService; } export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index 6949fba00c65a..d46af31b30667 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts index 8735fe7fe2668..59e3ada10a869 100644 --- a/src/plugins/presentation_util/public/services/kibana/dashboards.ts +++ b/src/plugins/presentation_util/public/services/kibana/dashboards.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; export type DashboardsServiceFactory = KibanaPluginServiceFactory< PresentationDashboardsService, diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts index 75388a71d14ca..880f0f8b49c76 100644 --- a/src/plugins/presentation_util/public/services/kibana/index.ts +++ b/src/plugins/presentation_util/public/services/kibana/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, KibanaPluginServiceParams, @@ -17,15 +18,17 @@ import { import { PresentationUtilPluginStartDeps } from '../../types'; import { PresentationUtilServices } from '..'; -export { dashboardsServiceFactory } from './dashboards'; export { capabilitiesServiceFactory } from './capabilities'; +export { dashboardsServiceFactory } from './dashboards'; +export { labsServiceFactory } from './labs'; export const providers: PluginServiceProviders< PresentationUtilServices, KibanaPluginServiceParams > = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), }; export const registry = new PluginServiceRegistry< diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts new file mode 100644 index 0000000000000..d2c0735c76eeb --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + environmentNames, + EnvironmentName, + projectIDs, + projects, + ProjectID, + Project, + getProjectIDs, +} from '../../../common'; +import { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { + PresentationLabsService, + isEnabledByStorageValue, + setStorageStatus, + setUISettingsStatus, + applyProjectStatus, +} from '../labs'; + +export type LabsServiceFactory = KibanaPluginServiceFactory< + PresentationLabsService, + PresentationUtilPluginStartDeps +>; + +export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { + const { uiSettings } = coreStart; + const localStorage = window.localStorage; + const sessionStorage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isEnabledByStorageValue(project, 'kibana', uiSettings.get(id, project.isActive)), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, status: boolean) => { + switch (env) { + case 'session': + setStorageStatus(sessionStorage, name, status); + break; + case 'browser': + setStorageStatus(localStorage, name, status); + break; + case 'kibana': + setUISettingsStatus(uiSettings, name, status); + break; + } + }; + + const reset = () => { + localStorage.clear(); + sessionStorage.clear(); + environmentNames.forEach((env) => + projectIDs.forEach((id) => setProjectStatus(id, env, projects[id].isActive)) + ); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts new file mode 100644 index 0000000000000..72e9a232ea976 --- /dev/null +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IUiSettingsClient } from 'kibana/public'; +import { + EnvironmentName, + projectIDs, + Project, + ProjectConfig, + ProjectID, + EnvironmentStatus, + environmentNames, + isProjectEnabledByStatus, +} from '../../common'; + +export interface PresentationLabsService { + getProjectIDs: () => typeof projectIDs; + getProject: (id: ProjectID) => Project; + getProjects: () => Record; + setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; + reset: () => void; +} + +export const isEnabledByStorageValue = ( + project: ProjectConfig, + environment: EnvironmentName, + value: string | boolean | null +): boolean => { + const defaultValue = project.isActive; + + if (!project.environments.includes(environment)) { + return defaultValue; + } + + if (value === true || value === false) { + return value; + } + + if (value === 'enabled') { + return true; + } + + if (value === 'disabled') { + return false; + } + + return defaultValue; +}; + +export const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => + storage.setItem(id, enabled ? 'enabled' : 'disabled'); + +export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { + const { isActive, environments } = project; + + environmentNames.forEach((name) => { + if (!environments.includes(name)) { + delete status[name]; + } + }); + + const isEnabled = isProjectEnabledByStatus(isActive, status); + const isOverride = isEnabled !== isActive; + + return { + ...project, + status: { + ...status, + defaultValue: isActive, + isEnabled, + isOverride, + }, + }; +}; + +export const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => + client.set(id, enabled); diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index 16fbe3baf488f..60285f00993ab 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -8,7 +8,7 @@ import { PluginServiceFactory } from '../create'; import { StorybookParams } from '.'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index dd7de54264062..37669d52c0096 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -8,6 +8,7 @@ import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create'; import { dashboardsServiceFactory } from '../stub/dashboards'; +import { labsServiceFactory } from './labs'; import { capabilitiesServiceFactory } from './capabilities'; import { PresentationUtilServices } from '..'; @@ -22,8 +23,9 @@ export interface StorybookParams { } export const providers: PluginServiceProviders = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts new file mode 100644 index 0000000000000..8878e218f19e8 --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EnvironmentName, projectIDs, Project } from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { projects, ProjectID, getProjectIDs } from '../../../common'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const storage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const { isActive } = project; + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isActive, + }; + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, enabled: boolean) => { + if (env === 'session') { + storage.setItem(name, enabled ? 'enabled' : 'disabled'); + } + }; + + const reset = () => { + storage.clear(); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 4154fa65a0cd7..80b913c4f0856 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory; diff --git a/src/plugins/presentation_util/public/services/stub/dashboards.ts b/src/plugins/presentation_util/public/services/stub/dashboards.ts index 280ae9582b815..047176836896b 100644 --- a/src/plugins/presentation_util/public/services/stub/dashboards.ts +++ b/src/plugins/presentation_util/public/services/stub/dashboards.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; // TODO (clint): Create set of dashboards to stub and return. diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts index d1a8147f8fb8c..6bf32bba00a3e 100644 --- a/src/plugins/presentation_util/public/services/stub/index.ts +++ b/src/plugins/presentation_util/public/services/stub/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { PresentationUtilServices } from '..'; @@ -17,6 +18,7 @@ export { capabilitiesServiceFactory } from './capabilities'; export const providers: PluginServiceProviders = { dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts new file mode 100644 index 0000000000000..c83bb68b5d072 --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/labs.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + projects, + projectIDs, + ProjectID, + EnvironmentName, + getProjectIDs, + Project, +} from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const reset = () => + projectIDs.reduce((acc, id) => { + const project = getProject(id); + const defaultValue = project.isActive; + + acc[id] = { + defaultValue, + session: null, + browser: null, + kibana: defaultValue, + }; + return acc; + }, {} as { [id in ProjectID]: { defaultValue: boolean; session: boolean | null; browser: boolean | null; kibana: boolean } }); + + let statuses = reset(); + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const value = statuses[id]; + const status = { + session: isEnabledByStorageValue(project, 'session', value.session), + browser: isEnabledByStorageValue(project, 'browser', value.browser), + kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { + statuses[id] = { ...statuses[id], [env]: value }; + }; + + return { + getProjectIDs, + getProject, + getProjects, + setProjectStatus, + reset: () => { + statuses = reset(); + }, + }; +}; diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index f1bd6c1b747eb..05779ffb206c4 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import { PresentationLabsService } from './services/labs'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginStart { ContextProvider: React.FC; + labsService: PresentationLabsService; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/core/server/legacy/config/index.ts b/src/plugins/presentation_util/server/index.ts similarity index 76% rename from src/core/server/legacy/config/index.ts rename to src/plugins/presentation_util/server/index.ts index b674b1386b786..de7e8de405442 100644 --- a/src/core/server/legacy/config/index.ts +++ b/src/plugins/presentation_util/server/index.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -export { ensureValidConfiguration } from './ensure_valid_configuration'; +import { PresentationUtilPlugin } from './plugin'; + +export const plugin = () => new PresentationUtilPlugin(); diff --git a/src/core/server/utils/package_json.ts b/src/plugins/presentation_util/server/plugin.ts similarity index 51% rename from src/core/server/utils/package_json.ts rename to src/plugins/presentation_util/server/plugin.ts index 57ca781d7d78e..eb55373920625 100644 --- a/src/core/server/utils/package_json.ts +++ b/src/plugins/presentation_util/server/plugin.ts @@ -6,10 +6,18 @@ * Side Public License, v 1. */ -import { dirname } from 'path'; +import { CoreSetup, Plugin } from 'kibana/server'; +import { getUISettings } from './ui_settings'; -export const pkg = { - __filename: require.resolve('../../../../package.json'), - __dirname: dirname(require.resolve('../../../../package.json')), - ...require('../../../../package.json'), -}; +export class PresentationUtilPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUISettings()); + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/presentation_util/server/ui_settings.ts b/src/plugins/presentation_util/server/ui_settings.ts new file mode 100644 index 0000000000000..450354832c3ac --- /dev/null +++ b/src/plugins/presentation_util/server/ui_settings.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { projects, projectIDs, ProjectID } from '../common'; + +export const SETTING_CATEGORY = 'Presentation Labs'; + +const labsProjectSettings: Record> = projectIDs.reduce( + (acc, id) => { + const project = projects[id]; + const { name, description, isActive: value } = project; + acc[id] = { + name, + value, + type: 'boolean', + description, + schema: schema.boolean(), + requiresPageReload: true, + category: [SETTING_CATEGORY], + }; + return acc; + }, + {} as { + [id in ProjectID]: UiSettingsParams; + } +); + +/** + * uiSettings definitions for Presentation Util. + */ +export const getUISettings = (): Record> => ({ + ...labsProjectSettings, +}); diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index cb39c5fb36f56..63d136cf9445a 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -7,9 +7,19 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "storybook/**/*", + "../../../typings/**/*" + ], "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../saved_objects/tsconfig.json" }, + { + "path": "../../core/tsconfig.json" + }, + { + "path": "../saved_objects/tsconfig.json" + }, ] } diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json index 385c31e6bd2d6..899611d027465 100644 --- a/src/plugins/region_map/tsconfig.json +++ b/src/plugins/region_map/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index 46b3f9a5afcb6..d9045b91b9dfa 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index cb74b06179225..99849dea38618 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json index ae566d9626895..b0059c71424bf 100644 --- a/src/plugins/saved_objects_tagging_oss/tsconfig.json +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json index 156e4ffee9d79..530e01a034b00 100644 --- a/src/plugins/security_oss/tsconfig.json +++ b/src/plugins/security_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index 62c0b3739f4b7..985066915f1dd 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json index 96584842ec32b..0cc82d7e5d124 100644 --- a/src/plugins/spaces_oss/tsconfig.json +++ b/src/plugins/spaces_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts index 84b6486f35b24..cc4ff102742d7 100644 --- a/src/plugins/telemetry/common/telemetry_config/index.ts +++ b/src/plugins/telemetry/common/telemetry_config/index.ts @@ -9,7 +9,5 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; -export { - getTelemetryFailureDetails, - TelemetryFailureDetails, -} from './get_telemetry_failure_details'; +export { getTelemetryFailureDetails } from './get_telemetry_failure_details'; +export type { TelemetryFailureDetails } from './get_telemetry_failure_details'; diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts index 6cca9bdf881dd..47ba7828eaec2 100644 --- a/src/plugins/telemetry/public/index.ts +++ b/src/plugins/telemetry/public/index.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { TelemetryPlugin, TelemetryPluginConfig } from './plugin'; -export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; +export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryPlugin(initializerContext); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 05ac1eb84089d..d8bcf150ac167 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8137,6 +8137,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "labs:presentation:unifiedToolbar": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index debdf7515cd58..1c335426ffd03 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; +export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, @@ -34,9 +34,12 @@ export { constants }; export { getClusterUuids, getLocalStats, - TelemetryLocalStats, DATA_TELEMETRY_ID, + buildDataTelemetryPayload, +} from './telemetry_collection'; + +export type { + TelemetryLocalStats, DataTelemetryIndex, DataTelemetryPayload, - buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index def1131dfb1a3..c93b7e872924b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -7,10 +7,5 @@ */ export { DATA_TELEMETRY_ID } from './constants'; - -export { - getDataTelemetry, - buildDataTelemetryPayload, - DataTelemetryPayload, - DataTelemetryIndex, -} from './get_data_telemetry'; +export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 55f9c7f0e624c..151e89a11a192 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -6,12 +6,9 @@ * Side Public License, v 1. */ -export { - DATA_TELEMETRY_ID, - DataTelemetryIndex, - DataTelemetryPayload, - buildDataTelemetryPayload, -} from './get_data_telemetry'; -export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; +export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry'; +export { getLocalStats } from './get_local_stats'; +export type { TelemetryLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts index 4e3f046f7611f..594b53259a65f 100644 --- a/src/plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/plugins/telemetry/server/telemetry_repository/index.ts @@ -8,7 +8,7 @@ export { getTelemetrySavedObject } from './get_telemetry_saved_object'; export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; -export { +export type { TelemetrySavedObject, TelemetrySavedObjectAttributes, } from '../../common/telemetry_config/types'; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 40370082f99e2..6629e479906c9 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/**/*", diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index 77077b73cf8ad..c0cd124a132c0 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryCollectionManagerPlugin(initializerContext); } -export { +export type { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, StatsCollectionConfig, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 8bbc440fb1a54..1329979860603 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "server/**/*", diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index 28b04418f512d..db6ea17556ed3 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export { TelemetryManagementSectionPluginSetup } from './plugin'; +export type { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index c6a21733b2d6b..2daee868ac200 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json index 385c31e6bd2d6..899611d027465 100644 --- a/src/plugins/tile_map/tsconfig.json +++ b/src/plugins/tile_map/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/timelion/tsconfig.json b/src/plugins/timelion/tsconfig.json index bb8a48339d44e..5b96d69a878ea 100644 --- a/src/plugins/timelion/tsconfig.json +++ b/src/plugins/timelion/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/ui_actions/tsconfig.json b/src/plugins/ui_actions/tsconfig.json index 89b66d18705c2..a871d7215cdc5 100644 --- a/src/plugins/ui_actions/tsconfig.json +++ b/src/plugins/ui_actions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json index f1916e4ce5957..8e867a6bad14f 100644 --- a/src/plugins/url_forwarding/tsconfig.json +++ b/src/plugins/url_forwarding/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index b9e0e0a8985b1..9b009b1d9e264 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; +export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 5f48f9fb93813..d5e0d95659e58 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -export { CollectorSet, CollectorSetPublic } from './collector_set'; -export { - Collector, +export { CollectorSet } from './collector_set'; +export type { CollectorSetPublic } from './collector_set'; +export { Collector } from './collector'; +export type { AllowedSchemaTypes, AllowedSchemaNumberTypes, SchemaField, @@ -16,4 +17,5 @@ export { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector, UsageCollectorOptions } from './usage_collector'; +export { UsageCollector } from './usage_collector'; +export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index dfc9d19b69646..dd9e6644a827d 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,17 +9,16 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { +export { Collector } from './collector'; +export type { AllowedSchemaTypes, MakeSchemaFrom, SchemaField, CollectorOptions, UsageCollectorOptions, - Collector, CollectorFetchContext, } from './collector'; - -export { UsageCollectionSetup } from './plugin'; +export type { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index 1a60d84e7948c..7e3f4273bbea8 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -16,7 +16,8 @@ import { import { CollectorOptions, Collector, UsageCollector } from './collector'; import { UsageCollectionSetup, CollectorFetchContext } from './index'; -export { CollectorOptions, Collector }; +export type { CollectorOptions }; +export { Collector }; const logger = loggingSystemMock.createLogger(); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index b4a0721ef3672..68a0853994e80 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/src/plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss index c412b9d915e55..56c6a0f0f63f6 100644 --- a/src/plugins/vis_default_editor/public/_default.scss +++ b/src/plugins/vis_default_editor/public/_default.scss @@ -1,6 +1,4 @@ .visEditor--default { - // height: 1px is in place to make editor children take their height in the parent - height: 1px; flex: 1 1 auto; display: flex; } @@ -80,6 +78,7 @@ .visEditor__collapsibleSidebar { width: 100% !important; // force the editor to take 100% width + flex-grow: 0; } .visEditor__collapsibleSidebar-isClosed { @@ -91,8 +90,10 @@ } .visEditor__visualization__wrapper { - // force the visualization to take 100% width and height. + // force the visualization to take 100% width. width: 100% !important; - height: 100% !important; + flex: 1; + display: flex; + flex-direction: column; } } diff --git a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx index 94f767510c4bd..277804567c2b7 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx @@ -11,7 +11,7 @@ import { act } from 'react-dom/test-utils'; import { mount, shallow, ReactWrapper } from 'enzyme'; import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui'; -import { IAggConfig, IndexPatternField } from 'src/plugins/data/public'; +import { IAggConfig, IndexPatternField, AggParam } from 'src/plugins/data/public'; import { ComboBoxGroupedOptions } from '../../utils'; import { FieldParamEditor, FieldParamEditorProps } from './field'; import { EditorVisState } from '../sidebar/state/reducers'; @@ -42,7 +42,7 @@ describe('FieldParamEditor component', () => { setTouched = jest.fn(); onChange = jest.fn(); - field = { displayName: 'bytes' } as IndexPatternField; + field = { displayName: 'bytes', type: 'bytes' } as IndexPatternField; option = { label: 'bytes', target: field }; indexedFields = [ { @@ -52,7 +52,16 @@ describe('FieldParamEditor component', () => { ]; defaultProps = { - agg: {} as IAggConfig, + agg: { + type: { + params: [ + ({ + name: 'field', + filterFieldTypes: ['bytes'], + } as unknown) as AggParam, + ], + }, + } as IAggConfig, aggParam: { name: 'field', type: 'field', diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index 95843dc6ae3a8..f8db2d89888a2 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -13,7 +13,13 @@ import useMount from 'react-use/lib/useMount'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggParam, IAggConfig, IFieldParamType, IndexPatternField } from 'src/plugins/data/public'; +import { + AggParam, + IAggConfig, + IFieldParamType, + IndexPatternField, + KBN_FIELD_TYPES, +} from '../../../../../plugins/data/public'; import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { ComboBoxGroupedOptions } from '../../utils'; @@ -55,6 +61,7 @@ function FieldParamEditor({ } }; const errors = customError ? [customError] : []; + let showErrorMessageImmediately = false; if (!indexedFields.length) { errors.push( @@ -69,9 +76,38 @@ function FieldParamEditor({ ); } + if (value && value.type === KBN_FIELD_TYPES.MISSING) { + errors.push( + i18n.translate('visDefaultEditor.controls.field.fieldIsNotExists', { + defaultMessage: + 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.', + values: { + fieldParameter: value.name, + }, + }) + ); + showErrorMessageImmediately = true; + } else if ( + value && + !getFieldTypes(agg).find((type: string) => type === value.type || type === '*') + ) { + errors.push( + i18n.translate('visDefaultEditor.controls.field.invalidFieldForAggregation', { + defaultMessage: + 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with this aggregation. Please select a new field.', + values: { + fieldParameter: value?.name, + indexPatternTitle: agg.getIndexPattern && agg.getIndexPattern().title, + }, + }) + ); + showErrorMessageImmediately = true; + } + const isValid = !!value && !errors.length && !isDirty; // we show an error message right away if there is no compatible fields - const showErrorMessage = (showValidation || !indexedFields.length) && !isValid; + const showErrorMessage = + (showValidation || !indexedFields.length || showErrorMessageImmediately) && !isValid; useValidation(setValidity, isValid); useMount(() => { @@ -122,10 +158,14 @@ function FieldParamEditor({ } function getFieldTypesString(agg: IAggConfig) { + return formatListAsProse(getFieldTypes(agg), { inclusive: false }); +} + +function getFieldTypes(agg: IAggConfig) { const param = get(agg, 'type.params', []).find((p: AggParam) => p.name === 'field') || ({} as IFieldParamType); - return formatListAsProse(parseCommaSeparatedList(param.filterFieldTypes), { inclusive: false }); + return parseCommaSeparatedList(param.filterFieldTypes || []); } export { FieldParamEditor }; diff --git a/src/plugins/vis_default_editor/tsconfig.json b/src/plugins/vis_default_editor/tsconfig.json index 54a84e08224a8..27bb775c2d0e8 100644 --- a/src/plugins/vis_default_editor/tsconfig.json +++ b/src/plugins/vis_default_editor/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_markdown/tsconfig.json b/src/plugins/vis_type_markdown/tsconfig.json index f940c295e7cee..d5ab89b98081b 100644 --- a/src/plugins/vis_type_markdown/tsconfig.json +++ b/src/plugins/vis_type_markdown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json index 8cee918a3dc82..7441848d5a430 100644 --- a/src/plugins/vis_type_metric/tsconfig.json +++ b/src/plugins/vis_type_metric/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index 4f2e80575497b..ccff3c349cf21 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json index f7f3688183a48..18bbad2257466 100644 --- a/src/plugins/vis_type_tagcloud/tsconfig.json +++ b/src/plugins/vis_type_tagcloud/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_timelion/tsconfig.json b/src/plugins/vis_type_timelion/tsconfig.json index d29fb25b15315..77f97de28366d 100644 --- a/src/plugins/vis_type_timelion/tsconfig.json +++ b/src/plugins/vis_type_timelion/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts index 515fadffb6b32..0428e6e80ae78 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts @@ -44,7 +44,7 @@ describe('extractIndexPatterns', () => { }); test('should return index patterns', () => { - expect(extractIndexPatternValues(panel, '')).toEqual([ + expect(extractIndexPatternValues(panel, null)).toEqual([ '*', 'example-1-*', 'example-2-*', diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts index 398d1c30ed5a7..af9f0750b2604 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts @@ -8,7 +8,7 @@ import { uniq } from 'lodash'; import { PanelSchema, IndexPatternValue, FetchedIndexPattern } from '../common/types'; -import { IndexPatternsService } from '../../data/common'; +import { IIndexPattern, IndexPatternsService } from '../../data/common'; export const isStringTypeIndexPattern = ( indexPatternValue: IndexPatternValue @@ -19,7 +19,7 @@ export const getIndexPatternKey = (indexPatternValue: IndexPatternValue) => export const extractIndexPatternValues = ( panel: PanelSchema, - defaultIndex?: PanelSchema['default_index_pattern'] + defaultIndex: IIndexPattern | null ) => { const patterns: IndexPatternValue[] = []; @@ -43,8 +43,8 @@ export const extractIndexPatternValues = ( }); } - if (patterns.length === 0 && defaultIndex) { - patterns.push(defaultIndex); + if (patterns.length === 0 && defaultIndex?.id) { + patterns.push({ id: defaultIndex.id }); } return uniq(patterns).sort(); diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 383b089593565..9fb7644b0fd16 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -213,8 +213,6 @@ export const panel = schema.object({ bar_color_rules: schema.maybe(arrayNullable), background_color: stringOptionalNullable, background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)), - default_index_pattern: stringOptionalNullable, - default_timefield: stringOptionalNullable, drilldown_url: stringOptional, drop_last_bucket: numberIntegerOptional, filter: schema.maybe(queryObject), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js index 42321c2728198..7a29db27a514f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js @@ -131,7 +131,7 @@ export function CalculationAgg(props) { CalculationAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index 8a597ffa9d5e8..d82bcbcd885cb 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -84,7 +84,7 @@ export function CumulativeSumAgg(props) { CumulativeSumAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index 8d155b378755a..6f7e5680b2a86 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -110,7 +110,7 @@ export const DerivativeAgg = (props) => { DerivativeAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 7f93567980b2d..90353f9af8e35 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -13,6 +13,7 @@ import { FieldSelect } from './field_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; import { htmlIdGenerator, @@ -29,7 +30,7 @@ import { getDataStart } from '../../../services'; import { QueryBarWrapper } from '../query_bar_wrapper'; const isFieldHistogram = (fields, indexPattern, field) => { - const indexFields = fields[indexPattern]; + const indexFields = fields[getIndexPatternKey(indexPattern)]; if (!indexFields) return false; const fieldObject = indexFields.find((f) => f.name === field); if (!fieldObject) return false; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js index 5fa9912ae17e7..e92659e677860 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js @@ -150,7 +150,7 @@ export function MathAgg(props) { MathAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index bf6c95202ed25..3c53e4597136e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -305,7 +305,7 @@ export const MovingAverageAgg = (props) => { MovingAverageAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 55c14e61bed1a..010a88146595b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -88,7 +88,7 @@ export const PositiveOnlyAgg = (props) => { PositiveOnlyAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index 00688992f819b..675a9868e13b3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -115,7 +115,7 @@ export const SerialDiffAgg = (props) => { SerialDiffAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index d3ff4f64b5351..bebc1cf2bce72 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -163,7 +163,7 @@ const StandardSiblingAggUi = (props) => { StandardSiblingAggUi.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 92e754c1dcdaf..12f7ad143cb25 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -26,6 +26,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; import { PANEL_TYPES } from '../../../../common/panel_types'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const isFieldTypeEnabled = (fieldRestrictions, fieldType) => fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; @@ -115,8 +116,8 @@ const TopHitAggUi = (props) => { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); const handleTextChange = createTextHandler(handleChange); - - const field = fields[indexPattern].find((f) => f.name === model.field); + const fieldsSelector = getIndexPatternKey(indexPattern); + const field = fields[fieldsSelector].find((f) => f.name === model.field); const aggWithOptions = getAggWithOptions(field, aggWithOptionsRestrictFields); const orderOptions = getOrderOptions(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js index ca310ab4153d1..b9d554e254bcc 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js @@ -96,7 +96,7 @@ CalculationVars.defaultProps = { CalculationVars.propTypes = { fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), metrics: PropTypes.array, model: PropTypes.object, name: PropTypes.string, diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index 0ad6344ac51b7..5a991238d10f8 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -29,6 +29,7 @@ import { YesNo } from './yes_no'; import { LastValueModePopover } from './last_value_mode_popover'; import { KBN_FIELD_TYPES } from '../../../../data/public'; import { FormValidationContext } from '../contexts/form_validation_context'; +import { DefaultIndexPatternContext } from '../contexts/default_index_context'; import { isGteInterval, validateReInterval, isAutoInterval } from './lib/get_interval'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -76,6 +77,7 @@ export const IndexPattern = ({ const intervalName = `${prefix}interval`; const maxBarsName = `${prefix}max_bars`; const dropBucketName = `${prefix}drop_last_bucket`; + const defaultIndex = useContext(DefaultIndexPatternContext); const updateControlValidity = useContext(FormValidationContext); const uiRestrictions = get(useContext(VisDataContext), 'uiRestrictions'); const maxBarsUiSettings = config.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); @@ -110,7 +112,6 @@ export const IndexPattern = ({ ]; const defaults = { - default_index_pattern: '', [indexPatternName]: '', [intervalName]: AUTO_INTERVAL, [dropBucketName]: 1, @@ -120,7 +121,6 @@ export const IndexPattern = ({ const model = { ...defaults, ..._model }; - const isDefaultIndexPatternUsed = model.default_index_pattern && !model[indexPatternName]; const intervalValidation = validateIntervalValue(model[intervalName]); const selectedTimeRangeOption = timeRangeOptions.find( ({ value }) => model[TIME_RANGE_MODE_KEY] === value @@ -205,7 +205,7 @@ export const IndexPattern = ({ onChange={handleSelectChange(timeFieldName)} indexPattern={model[indexPatternName]} fields={fields} - placeholder={isDefaultIndexPatternUsed ? model.default_timefield : undefined} + placeholder={defaultIndex?.timeFieldName} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx index 28b9c173a2b1b..ac86a299dfa1c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx @@ -23,6 +23,7 @@ import { FieldTextSelect } from './field_text_select'; import { ComboBoxSelect } from './combo_box_select'; import type { IndexPatternValue, FetchedIndexPattern } from '../../../../../common/types'; +import { DefaultIndexPatternContext } from '../../../contexts/default_index_context'; const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes'; @@ -61,6 +62,8 @@ export const IndexPatternSelect = ({ }: IndexPatternSelectProps) => { const htmlId = htmlIdGenerator(); const panelModel = useContext(PanelModelContext); + const defaultIndex = useContext(DefaultIndexPatternContext); + const [fetchedIndex, setFetchedIndex] = useState(); const useKibanaIndices = Boolean(panelModel?.[USE_KIBANA_INDEXES_KEY]); const Component = useKibanaIndices ? ComboBoxSelect : FieldTextSelect; @@ -146,7 +149,7 @@ export const IndexPatternSelect = ({ allowSwitchMode={allowIndexSwitchingMode} onIndexChange={onIndexChange} onModeChange={onModeChange} - placeholder={panelModel?.default_index_pattern ?? ''} + placeholder={defaultIndex?.title ?? ''} data-test-subj="metricsIndexPatternInput" /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.tsx index f39ff6923f5ce..99c3fa8ea9673 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.tsx @@ -152,7 +152,7 @@ export class GaugePanelConfig extends Component< onChange={(filter) => { this.props.onChange({ filter }); }} - indexPatterns={[model.index_pattern || model.default_index_pattern || '']} + indexPatterns={[model.index_pattern]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx index a9d9d01376608..c3f0f00125769 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx @@ -167,7 +167,7 @@ export class MarkdownPanelConfig extends Component< onChange={(filter) => { this.props.onChange({ filter }); }} - indexPatterns={[model.index_pattern || model.default_index_pattern || '']} + indexPatterns={[model.index_pattern]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.tsx index 3ab49c1bef873..f38d0ec83e957 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.tsx @@ -116,7 +116,7 @@ export class MetricPanelConfig extends Component< onChange={(filter) => { this.props.onChange({ filter }); }} - indexPatterns={[model.index_pattern || model.default_index_pattern || '']} + indexPatterns={[model.index_pattern]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx index f3d01df19666a..1a78585de433a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx @@ -244,7 +244,7 @@ export class TablePanelConfig extends Component< onChange={(filter) => { this.props.onChange({ filter }); }} - indexPatterns={[model.index_pattern || model.default_index_pattern || '']} + indexPatterns={[model.index_pattern]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx index 2e714b8db480b..ae36408a08b46 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx @@ -207,7 +207,7 @@ export class TimeseriesPanelConfig extends Component< onChange={(filter) => { this.props.onChange({ filter }); }} - indexPatterns={[model.index_pattern || model.default_index_pattern || '']} + indexPatterns={[model.index_pattern]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.tsx index 78ac11eb39744..a537a769cac11 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.tsx @@ -144,7 +144,7 @@ export class TopNPanelConfig extends Component< onChange={(filter: PanelConfigProps['model']['filter']) => { this.props.onChange({ filter }); }} - indexPatterns={[model.index_pattern || model.default_index_pattern || '']} + indexPatterns={[model.index_pattern]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx b/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx index f9a5de313521a..ccbfebe84bc51 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx @@ -9,6 +9,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { CoreStartContext } from '../contexts/query_input_bar_context'; +import { DefaultIndexPatternContext } from '../contexts/default_index_context'; import { IndexPatternValue } from '../../../common/types'; import { QueryStringInput, QueryStringInputProps } from '../../../../../plugins/data/public'; @@ -24,27 +25,32 @@ export function QueryBarWrapper({ query, onChange, indexPatterns }: QueryBarWrap const [indexes, setIndexes] = useState([]); const coreStartContext = useContext(CoreStartContext); + const defaultIndex = useContext(DefaultIndexPatternContext); useEffect(() => { async function fetchIndexes() { const i: QueryStringInputProps['indexPatterns'] = []; for (const index of indexPatterns ?? []) { - if (isStringTypeIndexPattern(index)) { - i.push(index); - } else if (index?.id) { - const fetchedIndex = await fetchIndexPattern(index, indexPatternsService); - - if (fetchedIndex.indexPattern) { - i.push(fetchedIndex.indexPattern); + if (index) { + if (isStringTypeIndexPattern(index)) { + i.push(index); + } else if (index?.id) { + const { indexPattern } = await fetchIndexPattern(index, indexPatternsService); + + if (indexPattern) { + i.push(indexPattern); + } } + } else if (defaultIndex) { + i.push(defaultIndex); } } setIndexes(i); } fetchIndexes(); - }, [indexPatterns, indexPatternsService]); + }, [indexPatterns, indexPatternsService, defaultIndex]); return ( ); }} diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index b996abd6373ab..ab5342e925bd7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -237,7 +237,7 @@ SplitByTermsUI.propTypes = { intl: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), fields: PropTypes.object, uiRestrictions: PropTypes.object, seriesQuantity: PropTypes.object, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index 887075e9e4e48..90b7ccaa14d86 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -12,17 +12,19 @@ import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; import { EventEmitter } from 'events'; -import { IUiSettingsClient } from 'kibana/public'; -import { TimeRange } from 'src/plugins/data/public'; +import type { IUiSettingsClient } from 'kibana/public'; import { Vis, PersistedState, VisualizeEmbeddableContract, -} from 'src/plugins/visualizations/public'; -import { IndexPatternValue, TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; +} from '../../../../../plugins/visualizations/public'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; +import { DefaultIndexPatternContext } from '../contexts/default_index_context'; import { Storage } from '../../../../../plugins/kibana_utils/public'; +import type { IIndexPattern, TimeRange } from '../../../../../plugins/data/public'; +import type { IndexPatternValue, TimeseriesVisData } from '../../../common/types'; + // @ts-expect-error import { VisEditorVisualization } from './vis_editor_visualization'; import { PanelConfig } from './panel_config'; @@ -48,6 +50,7 @@ export interface TimeseriesEditorProps { interface TimeseriesEditorState { autoApply: boolean; dirty: boolean; + defaultIndex: IIndexPattern | null; extractedIndexPatterns: IndexPatternValue[]; model: TimeseriesVisParams; visFields?: VisFields; @@ -65,6 +68,7 @@ export class VisEditor extends Component { @@ -175,35 +179,37 @@ export class VisEditor extends Component -
    -
    - -
    - -
    - +
    +
    + +
    + +
    + +
    -
    + ); } @@ -212,24 +218,13 @@ export class VisEditor extends Component { - const defaultIndexTitle = index?.title ?? ''; - const indexPatterns = extractIndexPatternValues(this.props.vis.params, defaultIndexTitle); + const indexPatterns = extractIndexPatternValues(this.props.vis.params, index); const visFields = await fetchFields(indexPatterns); - this.setState((state) => ({ - model: { - ...state.model, - /** @legacy - * please use IndexPatterns service instead - * **/ - default_index_pattern: defaultIndexTitle, - /** @legacy - * please use IndexPatterns service instead - * **/ - default_timefield: index?.timeFieldName ?? '', - }, + this.setState({ + defaultIndex: index, visFields, - })); + }); }); this.props.eventEmitter.on('updateEditor', this.updateModel); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss index 198f0f42d503c..8689dbb4e1870 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss @@ -2,10 +2,14 @@ display: flex; flex-direction: column; flex: 1 1 100%; - overflow: auto; + position: relative; .tvbVisTimeSeries { - overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; } .tvbVisTimeSeriesDark { .echReactiveChart_unavailable { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index ae3fa4d9dcca4..e3a3902ce1baa 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -225,21 +225,23 @@ class TimeseriesVisualization extends Component { return (
    - +
    + +
    ); } diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/default_index_context.ts b/src/plugins/vis_type_timeseries/public/application/contexts/default_index_context.ts new file mode 100644 index 0000000000000..a8770d86fba9b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/contexts/default_index_context.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { IIndexPattern } from '../../../../data/public'; + +export const DefaultIndexPatternContext = React.createContext(null); diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index a90faea50f22a..f9a52a9450dcb 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -16,7 +16,7 @@ import { Chart, Position, Settings, - AnnotationDomainTypes, + AnnotationDomainType, LineAnnotation, TooltipType, StackMode, @@ -86,7 +86,7 @@ export const TimeSeries = ({ const hasBarChart = series.some(({ bars }) => bars?.show); // apply legend style change if bgColor is configured - const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + const classes = classNames(getChartClasses(backgroundColor)); // If the color isn't configured by the user, use the color mapping service // to assign a color from the Kibana palette. Colors will be shared across the @@ -149,6 +149,7 @@ export const TimeSeries = ({ tooltip={{ snap: true, type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, + boundary: document.getElementById('app-fixed-viewport') ?? undefined, headerFormatter: tooltipFormatter, }} externalPointerEvents={{ tooltip: { visible: false } }} @@ -162,7 +163,7 @@ export const TimeSeries = ({ } hideLinesTooltips={true} diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json index edc2d25b867d1..7b2dd4b608c1c 100644 --- a/src/plugins/vis_type_timeseries/tsconfig.json +++ b/src/plugins/vis_type_timeseries/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index f375a2483e24f..4091dafcbe357 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_vislib/tsconfig.json b/src/plugins/vis_type_vislib/tsconfig.json index 845628a6b86f9..74bc1440d9dbc 100644 --- a/src/plugins/vis_type_vislib/tsconfig.json +++ b/src/plugins/vis_type_vislib/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 59bed0060a6a6..8922f512522a0 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -148,13 +148,15 @@ export const XYSettings: FC = ({ : headerValueFormatter && (tooltip.detailedTooltip ? undefined : ({ value }: any) => headerValueFormatter(value)); + const boundary = document.getElementById('app-fixed-viewport') ?? undefined; const tooltipProps: TooltipProps = tooltip.detailedTooltip ? { ...tooltip, + boundary, customTooltip: tooltip.detailedTooltip(headerFormatter), headerFormatter: undefined, } - : { ...tooltip, headerFormatter }; + : { ...tooltip, boundary, headerFormatter }; return ( = ({ { + this.getUpdated$().subscribe((value) => { const isDirty = this.handleChanges(); + if (isDirty && this.handler) { this.updateHandler(); } @@ -367,8 +368,8 @@ export class VisualizeEmbeddable } } - public reload = () => { - this.handleVisUpdate(); + public reload = async () => { + await this.handleVisUpdate(); }; private async updateHandler() { @@ -395,13 +396,13 @@ export class VisualizeEmbeddable }); if (this.handler && !abortController.signal.aborted) { - this.handler.update(this.expression, expressionParams); + await this.handler.update(this.expression, expressionParams); } } private handleVisUpdate = async () => { this.handleChanges(); - this.updateHandler(); + await this.updateHandler(); }; private uiStateChangeHandler = () => { diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index e7410c7a97343..b082737cce288 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1953,4 +1953,28 @@ describe('migration visualization', () => { expect(hideLastValueIndicator).toBeUndefined(); }); }); + + describe('7.13.0 tsvb - remove default_index_pattern and default_timefield from Model', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.13.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const createTestDocWithType = () => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{"type":"metrics","params":{"default_index_pattern":"test", "default_timefield":"test"}}`, + }, + }); + + it('should remove default_index_pattern and default_timefield', () => { + const migratedTestDoc = migrate(createTestDocWithType()); + const { params } = JSON.parse(migratedTestDoc.attributes.visState); + + expect(params).not.toHaveProperty('default_index_pattern'); + expect(params).not.toHaveProperty('default_timefield'); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 633442ec55d69..093255d65a7a8 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -951,6 +951,36 @@ const hideTSVBLastValueIndicator: SavedObjectMigrationFn = (doc) => { return doc; }; +const removeDefaultIndexPatternAndTimeFieldFromTSVBModel: SavedObjectMigrationFn = ( + doc +) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics') { + const { params } = visState; + + delete params.default_index_pattern; + delete params.default_timefield; + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + export const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -986,5 +1016,9 @@ export const visualizationSavedObjectTypeMigrations = { '7.10.0': flow(migrateFilterRatioQuery, removeTSVBSearchSource), '7.11.0': flow(enableDataTableVisToolbar), '7.12.0': flow(migrateVislibAreaLineBarTypes, migrateSchema), - '7.13.0': flow(addSupportOfDualIndexSelectionModeInTSVB, hideTSVBLastValueIndicator), + '7.13.0': flow( + addSupportOfDualIndexSelectionModeInTSVB, + hideTSVBLastValueIndicator, + removeDefaultIndexPatternAndTimeFieldFromTSVBModel + ), }; diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 65de6908228b3..d7c5e6a4b4366 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 256e634ac6c40..f6ef1caf9c9e0 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -183,8 +183,12 @@ const TopNav = ({ useEffect(() => { const autoRefreshFetchSub = services.data.query.timefilter.timefilter .getAutoRefreshFetch$() - .subscribe(() => { - visInstance.embeddableHandler.reload(); + .subscribe(async (done) => { + try { + await visInstance.embeddableHandler.reload(); + } finally { + done(); + } }); return () => { autoRefreshFetchSub.unsubscribe(); diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index e696bcb5dbe4d..b7c7d63cef98f 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -18,7 +18,10 @@ import { SavedObjectSaveOpts, OnSaveProps, } from '../../../../saved_objects/public'; -import { SavedObjectSaveModalDashboard } from '../../../../presentation_util/public'; +import { + LazySavedObjectSaveModalDashboard, + withSuspense, +} from '../../../../presentation_util/public'; import { unhashUrl } from '../../../../kibana_utils/public'; import { @@ -52,6 +55,8 @@ interface TopNavConfigParams { embeddableId?: string; } +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); + export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => { if (!anonymousUserCapabilities.visualize) return false; @@ -420,40 +425,47 @@ export const getTopNavConfig = ( const useByRefFlow = !!originatingApp || !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; - const saveModal = useByRefFlow ? ( - {}} - originatingApp={originatingApp} - returnToOriginSwitchLabel={ - originatingApp && embeddableId - ? i18n.translate('visualize.topNavMenu.updatePanel', { - defaultMessage: 'Update panel on {originatingAppName}', - values: { - originatingAppName: stateTransfer.getAppNameFromId(originatingApp), - }, - }) - : undefined - } - /> - ) : ( - {}} - /> - ); + let saveModal; + + if (useByRefFlow) { + saveModal = ( + {}} + originatingApp={originatingApp} + returnToOriginSwitchLabel={ + originatingApp && embeddableId + ? i18n.translate('visualize.topNavMenu.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { + originatingAppName: stateTransfer.getAppNameFromId(originatingApp), + }, + }) + : undefined + } + /> + ); + } else { + saveModal = ( + {}} + /> + ); + } + showSaveModal( saveModal, I18nContext, diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index cc0f3ce2afae5..9eda709e58c3e 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -18,8 +18,17 @@ import { SavedObject } from 'src/plugins/saved_objects/public'; import { cloneDeep } from 'lodash'; import { ExpressionValueError } from 'src/plugins/expressions/public'; import { createSavedSearchesLoader } from '../../../../discover/public'; +import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common'; import { VisualizeServices } from '../types'; +function isErrorRelatedToRuntimeFields(error: ExpressionValueError['error']) { + const originalError = error.original || error; + return ( + originalError instanceof SavedFieldNotFound || + originalError instanceof SavedFieldTypeInvalidForAgg + ); +} + const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices @@ -37,7 +46,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( })) as VisualizeEmbeddableContract; embeddableHandler.getOutput$().subscribe((output) => { - if (output.error) { + if (output.error && !isErrorRelatedToRuntimeFields(output.error)) { data.search.showError( ((output.error as unknown) as ExpressionValueError['error']).original || output.error ); diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 64d61996495d7..965951bfbd88d 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -11,13 +11,12 @@ import { EventEmitter } from 'events'; import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; -import { redirectWhenMissing } from '../../../../../kibana_utils/public'; - import { getVisualizationInstance } from '../get_visualization_instance'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; import { SavedVisInstance, VisualizeServices, IEditorController } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; import { getVisEditorsRegistry } from '../../../services'; +import { redirectToSavedObjectPage } from '../utils'; /** * This effect is responsible for instantiating a saved vis or creating a new one @@ -43,9 +42,7 @@ export const useSavedVisInstance = ( chrome, history, dashboard, - setActiveUrl, toastNotifications, - http: { basePath }, stateTransferService, application: { navigateToApp }, } = services; @@ -131,27 +128,8 @@ export const useSavedVisInstance = ( visEditorController, }); } catch (error) { - const managementRedirectTarget = { - app: 'management', - path: `kibana/objects/savedVisualizations/${visualizationIdFromUrl}`, - }; - try { - redirectWhenMissing({ - history, - navigateToApp, - toastNotifications, - basePath, - mapping: { - visualization: VisualizeConstants.LANDING_PAGE_PATH, - search: managementRedirectTarget, - 'index-pattern': managementRedirectTarget, - 'index-pattern-field': managementRedirectTarget, - }, - onBeforeRedirect() { - setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH); - }, - })(error); + redirectToSavedObjectPage(services, error, visualizationIdFromUrl); } catch (e) { toastNotifications.addWarning({ title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts index 0e529507f97e3..c906ff5304c90 100644 --- a/src/plugins/visualize/public/application/utils/utils.ts +++ b/src/plugins/visualize/public/application/utils/utils.ts @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ChromeStart, DocLinksStart } from 'kibana/public'; import { Filter } from '../../../../data/public'; +import { redirectWhenMissing } from '../../../../kibana_utils/public'; +import { VisualizeConstants } from '../visualize_constants'; import { VisualizeServices, VisualizeEditorVisInstance } from '../types'; export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => { @@ -58,3 +60,36 @@ export const visStateToEditorState = ( linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId, }; }; + +export const redirectToSavedObjectPage = ( + services: VisualizeServices, + error: any, + savedVisualizationsId?: string +) => { + const { + history, + setActiveUrl, + toastNotifications, + http: { basePath }, + application: { navigateToApp }, + } = services; + const managementRedirectTarget = { + app: 'management', + path: `kibana/objects/savedVisualizations/${savedVisualizationsId}`, + }; + redirectWhenMissing({ + history, + navigateToApp, + toastNotifications, + basePath, + mapping: { + visualization: VisualizeConstants.LANDING_PAGE_PATH, + search: managementRedirectTarget, + 'index-pattern': managementRedirectTarget, + 'index-pattern-field': managementRedirectTarget, + }, + onBeforeRedirect() { + setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH); + }, + })(error); +}; diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 046202d82d1aa..bc0891f391746 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/dashboard_filtering.ts index e995bc4e52c49..86c57efec818b 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/dashboard_filtering.ts @@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); - describe('dashboard filtering', function () { + // Failing: See https://github.com/elastic/kibana/issues/92522 + describe.skip('dashboard filtering', function () { this.tags('includeFirefox'); const populateDashboard = async () => { diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 9726b097c8f62..1d65b9a68bd4d 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - describe('saved queries saved objects', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/89477 + describe.skip('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index e3ff1819aed13..e2227d4240d40 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const testSubjects = getService('testSubjects'); - describe('runtime fields', function () { + // Failing: See https://github.com/elastic/kibana/issues/95376 + describe.skip('runtime fields', function () { this.tags(['skipFirefox']); before(async function () { diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 668aec6ac5783..3af467affa1fb 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -15,7 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const PageObjects = getPageObjects([ 'common', @@ -221,63 +220,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); - - describe('zoom warning behavior', function describeIndexTests() { - // Zoom warning is only applicable to OSS - this.tags(['skipCloud', 'skipFirefox']); - - const waitForLoading = false; - let zoomWarningEnabled; - let last = false; - const toastDefaultLife = 6000; - - before(async function () { - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - - zoomWarningEnabled = await testSubjects.exists('zoomWarningEnabled'); - log.debug(`Zoom warning enabled: ${zoomWarningEnabled}`); - - const zoomLevel = 9; - for (let i = 0; i < zoomLevel; i++) { - await PageObjects.tileMap.clickMapZoomIn(); - } - }); - - beforeEach(async function () { - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - }); - - afterEach(async function () { - if (!last) { - await PageObjects.common.sleep(toastDefaultLife); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - } - }); - - it('should show warning at zoom 10', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should continue providing zoom warning if left alone', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should suppress zoom warning if suppress warnings button clicked', async () => { - last = true; - await PageObjects.visChart.waitForVisualization(); - await testSubjects.click('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - await testSubjects.waitForDeleted('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - - await testSubjects.missingOrFail('maxZoomWarning'); - }); - }); }); } diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index dab12de2cef6b..d0a714d6759b5 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -9,7 +9,7 @@ import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils'; import testSubjSelector from '@kbn/test-subj-selector'; import { Test } from '@kbn/test/types/ftr'; -import { pkg } from '../../../../src/core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; // @ts-ignore internal js that is passed to the browser as is @@ -45,6 +45,7 @@ export async function VisualTestingProvider({ getService }: FtrProviderContext) }); const statsCache = new WeakMap(); + function getStats(test: Test) { if (!statsCache.has(test)) { statsCache.set(test, { diff --git a/tsconfig.base.json b/tsconfig.base.json index c28ed0d8c8750..da4de5ef3712b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -56,6 +56,5 @@ "jest-styled-components", "@testing-library/jest-dom" ] - }, - "include": [] + } } diff --git a/tsconfig.json b/tsconfig.json index 30944ac71fcc8..40763ede1bbdd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,79 +19,6 @@ "x-pack/plugins/cases/**/*", "x-pack/plugins/lists/**/*", "x-pack/plugins/security_solution/**/*", - - // tests - "src/**/*.test.ts", - "src/**/*.test.tsx", - "src/**/integration_tests/*", - "src/**/tests/*", - // mocks - "src/**/__mocks__/*", - "src/**/mock/*", - "src/**/mocks/*", - "src/**/*/mock.ts", - "src/**/*/mocks.ts", - "src/**/*/mocks.tsx", - "src/**/*.mock.ts", - "src/**/*.mock.tsx", - "src/**/*.mocks.ts", - "src/**/*.mocks.tsx", - - // test helpers - "src/**/test_helpers/*", - "src/**/test_utils/*", - "src/**/*/test_utils.ts", - "src/**/*/test_helpers.ts", - "src/**/*/test_helper.tsx", - - // stubs - "src/**/*/stubs.ts", - "src/**/*.stub.ts", - "src/**/*.stories.tsx", - "src/**/*/_mock_handler_arguments.ts", - - // tests - "x-pack/plugins/**/*.test.ts", - "x-pack/plugins/**/*.test.tsx", - "x-pack/plugins/**/test/**/*", - "x-pack/plugins/**/tests/*", - "x-pack/plugins/**/integration_tests/*", - "x-pack/plugins/**/tests_client_integration/*", - "x-pack/plugins/**/__fixtures__/*", - "x-pack/plugins/**/__stories__/*", - "x-pack/plugins/**/__jest__/**/*", - - // mocks - "x-pack/plugins/**/__mocks__/*", - "x-pack/plugins/**/mock/*", - "x-pack/plugins/**/mocks/*", - "x-pack/plugins/**/*/mock.ts", - "x-pack/plugins/**/*/mocks.ts", - "x-pack/plugins/**/*/mocks.tsx", - "x-pack/plugins/**/*.mock.ts", - "x-pack/plugins/**/*.mock.tsx", - "x-pack/plugins/**/*.mocks.ts", - "x-pack/plugins/**/*.mocks.tsx", - - // test helpers - "x-pack/plugins/**/test_helpers/*", - "x-pack/plugins/**/test_utils/*", - "x-pack/plugins/**/*/test_utils.ts", - "x-pack/plugins/**/*/test_helper.tsx", - "x-pack/plugins/**/*/test_helpers.ts", - "x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx", - "x-pack/plugins/uptime/server/lib/requests/helper.ts", - "x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx", - "x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx", - "x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx", - "x-pack/plugins/apm/server/utils/test_helpers.tsx", - "x-pack/plugins/apm/public/utils/testHelpers.tsx", - - // stubs - "x-pack/plugins/**/*/stubs.ts", - "x-pack/plugins/**/*.stub.ts", - "x-pack/plugins/**/*.stories.tsx", - "x-pack/plugins/**/*/_mock_handler_arguments.ts" ], "exclude": [ "x-pack/plugins/security_solution/cypress/**/*" @@ -181,7 +108,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/tsconfig.project.json b/tsconfig.project.json deleted file mode 100644 index 174c3fdf0fd54..0000000000000 --- a/tsconfig.project.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true, - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "exclude": [ - // tests - "**/*.test.ts", - "**/*.test.tsx", - "**/integration_tests/*", - "**/test/**/*", - "**/test/*", - "**/tests/*", - "**/tests_client_integration/*", - "**/__fixtures__/*", - "**/__stories__/*", - "**/__jest__/**", - - // mocks - "**/__mocks__/*", - "**/mock/*", - "**/mocks/*", - "**/*/mock.ts", - "**/*/mocks.ts", - "**/*/mocks.tsx", - "**/*.mock.ts", - "**/*.mock.tsx", - "**/*.mocks.ts", - "**/*.mocks.tsx", - - // test helpers - "**/test_helpers/*", - "**/test_utils/*", - "**/*/test_utils.ts", - "**/*/test_helper.tsx", - "**/*/test_helpers.ts", - // x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx - "**/*/test_data.tsx", - "**/*/shared_columns_tests.tsx", - // x-pack/plugins/uptime/server/lib/requests/helper.ts - "**/*/requests/helper.ts", - // x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx - "**/*/rtl_helpers.tsx", - // x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx - "**/*/enzyme_helpers.tsx", - // x-pack/plugins/apm/server/utils/test_helpers.tsx - "**/*/test_helpers.tsx", - // x-pack/plugins/apm/public/utils/testHelpers.tsx - "**/*/testHelpers.tsx", - - // stubs - "**/*/stubs.ts", - "**/*.stub.ts", - "**/*.stories.tsx", - "**/*/_mock_handler_arguments.ts" - ], - "include": [], - "types": [ - "node", - "flot" - ] -} diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 2d9ddc1b9e568..f13455a14b4df 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -85,7 +85,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts index db9c996147c94..f6131679874db 100644 --- a/x-pack/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -33,7 +33,7 @@ export class AlertingExamplePlugin implements Plugin { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration('60s'), }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index 012cd63be2702..76f6a62ce6597 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -17,6 +17,10 @@ const createActionsConfigMock = () => { ensureActionTypeEnabled: jest.fn().mockReturnValue({}), isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true), getProxySettings: jest.fn().mockReturnValue(undefined), + getResponseSettings: jest.fn().mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index cae6777a82441..c81f1f4a4bf2e 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { ActionsConfig } from './config'; import { getActionsConfigurationUtilities, AllowedHosts, EnabledActionTypes, } from './actions_config'; +import moment from 'moment'; const defaultActionsConfig: ActionsConfig = { enabled: false, @@ -19,6 +21,8 @@ const defaultActionsConfig: ActionsConfig = { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }; describe('ensureUriAllowed', () => { @@ -253,3 +257,94 @@ describe('ensureActionTypeEnabled', () => { expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); }); }); + +describe('getResponseSettingsFromConfig', () => { + test('returns expected parsed values for default config for responseTimeout and maxResponseContentLength', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + }; + expect(getActionsConfigurationUtilities(config).getResponseSettings()).toEqual({ + timeout: 60000, + maxContentLength: 1000000, + }); + }); +}); + +describe('getProxySettings', () => { + test('returns undefined when no proxy URL set', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyHeaders: { someHeaderName: 'some header value' }, + proxyBypassHosts: ['avoid-proxy.co'], + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings).toBeUndefined(); + }); + + test('returns proxy url', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + }; + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyUrl).toBe(config.proxyUrl); + }); + + test('returns proxyRejectUnauthorizedCertificates', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyRejectUnauthorizedCertificates: true, + }; + let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); + expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyRejectUnauthorizedCertificates: false, + }; + proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); + expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false); + }); + + test('returns proxy headers', () => { + const proxyHeaders = { + someHeaderName: 'some header value', + someOtherHeader: 'some other header', + }; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyHeaders, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyHeaders).toEqual(config.proxyHeaders); + }); + + test('returns proxy bypass hosts', () => { + const proxyBypassHosts = ['proxy-bypass-1.elastic.co', 'proxy-bypass-2.elastic.co']; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyBypassHosts, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyBypassHosts).toEqual(new Set(proxyBypassHosts)); + }); + + test('returns proxy only hosts', () => { + const proxyOnlyHosts = ['proxy-only-1.elastic.co', 'proxy-only-2.elastic.co']; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyOnlyHosts, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyOnlyHosts).toEqual(new Set(proxyOnlyHosts)); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index 2787f8f971101..4c73cab76f9e8 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -11,17 +11,11 @@ import url from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; -import { ActionsConfig } from './config'; +import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config'; import { ActionTypeDisabledError } from './lib'; -import { ProxySettings } from './types'; +import { ProxySettings, ResponseSettings } from './types'; -export enum AllowedHosts { - Any = '*', -} - -export enum EnabledActionTypes { - Any = '*', -} +export { AllowedHosts, EnabledActionTypes } from './config'; enum AllowListingField { URL = 'url', @@ -37,6 +31,7 @@ export interface ActionsConfigurationUtilities { ensureActionTypeEnabled: (actionType: string) => void; isRejectUnauthorizedCertificatesEnabled: () => boolean; getProxySettings: () => undefined | ProxySettings; + getResponseSettings: () => ResponseSettings; } function allowListErrorMessage(field: AllowListingField, value: string) { @@ -93,11 +88,25 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet return { proxyUrl: config.proxyUrl, + proxyBypassHosts: arrayAsSet(config.proxyBypassHosts), + proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts), proxyHeaders: config.proxyHeaders, proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates, }; } +function arrayAsSet(arr: T[] | undefined): Set | undefined { + if (!arr) return; + return new Set(arr); +} + +function getResponseSettingsFromConfig(config: ActionsConfig): ResponseSettings { + return { + maxContentLength: config.maxResponseContentLength.getValueInBytes(), + timeout: config.responseTimeout.asMilliseconds(), + }; +} + export function getActionsConfigurationUtilities( config: ActionsConfig ): ActionsConfigurationUtilities { @@ -109,6 +118,7 @@ export function getActionsConfigurationUtilities( isUriAllowed, isActionTypeEnabled, getProxySettings: () => getProxySettingsFromConfig(config), + getResponseSettings: () => getResponseSettingsFromConfig(config), isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized, ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index b858d5491a6bd..4596619c50940 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -283,6 +283,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -342,6 +343,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 6a67f4f6752c2..edc9429e4fac6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -7,12 +7,16 @@ import axios from 'axios'; import { Agent as HttpsAgent } from 'https'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; import { getCustomAgents } from './get_custom_agents'; +const TestUrl = 'https://elastic.co/foo/bar/baz'; + const logger = loggingSystemMock.create().get() as jest.Mocked; const configurationUtilities = actionsConfigMock.create(); jest.mock('axios'); @@ -38,6 +42,10 @@ describe('request', () => { headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, })); + configurationUtilities.getResponseSettings.mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }); }); test('it fetch correctly with defaults', async () => { @@ -54,6 +62,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -66,22 +76,26 @@ describe('request', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyRejectUnauthorizedCertificates: true, proxyUrl: 'https://localhost:1212', + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, TestUrl); const res = await request({ axios, - url: 'http://testProxy', + url: TestUrl, logger, configurationUtilities, }); - expect(axiosMock).toHaveBeenCalledWith('http://testProxy', { + expect(axiosMock).toHaveBeenCalledWith(TestUrl, { method: 'get', data: {}, httpAgent, httpsAgent, proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -94,6 +108,8 @@ describe('request', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope:', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const res = await request({ axios, @@ -108,6 +124,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -116,6 +134,90 @@ describe('request', () => { }); }); + test('it bypasses with proxyBypassHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: new Set(['elastic.co']), + proxyOnlyHosts: undefined, + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(false); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(false); + }); + + test('it does not bypass with proxyBypassHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: new Set(['not-elastic.co']), + proxyOnlyHosts: undefined, + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(true); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(true); + }); + + test('it proxies with proxyOnlyHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['elastic.co']), + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(true); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(true); + }); + + test('it does not proxy with proxyOnlyHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-elastic.co']), + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(false); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(false); + }); + test('it fetch correctly', async () => { const res = await request({ axios, @@ -132,6 +234,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -143,10 +247,15 @@ describe('request', () => { describe('patch', () => { beforeEach(() => { + jest.resetAllMocks(); axiosMock.mockImplementation(() => ({ status: 200, headers: { 'content-type': 'application/json' }, })); + configurationUtilities.getResponseSettings.mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }); }); test('it fetch correctly', async () => { @@ -157,6 +266,8 @@ describe('patch', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index f86f3b86c506a..af353e1d1da5a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -30,7 +30,8 @@ export const request = async ({ validateStatus?: (status: number) => boolean; auth?: AxiosBasicCredentials; }): Promise => { - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url); + const { maxContentLength, timeout } = configurationUtilities.getResponseSettings(); return await axios(url, { ...rest, @@ -40,6 +41,8 @@ export const request = async ({ httpAgent, httpsAgent, proxy: false, + maxContentLength, + timeout, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts index 340ac0f6dda3a..f6d1be9bffc6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts @@ -14,6 +14,10 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; const logger = loggingSystemMock.create().get() as jest.Mocked; +const targetHost = 'elastic.co'; +const targetUrl = `https://${targetHost}/foo/bar/baz`; +const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`; + describe('getCustomAgents', () => { const configurationUtilities = actionsConfigMock.create(); @@ -21,8 +25,10 @@ describe('getCustomAgents', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); }); @@ -31,15 +37,73 @@ describe('getCustomAgents', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope: not a valid URL', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent).toBe(undefined); expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); test('return default agents for undefined proxy options', () => { - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent).toBe(undefined); expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); + + test('returns non-proxy agents for matching proxyBypassHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set([targetHost]), + proxyOnlyHosts: undefined, + }); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); + expect(httpAgent instanceof HttpProxyAgent).toBeFalsy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy(); + }); + + test('returns proxy agents for non-matching proxyBypassHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set([targetHost]), + proxyOnlyHosts: undefined, + }); + const { httpAgent, httpsAgent } = getCustomAgents( + configurationUtilities, + logger, + nonMatchingUrl + ); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('returns proxy agents for matching proxyOnlyHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set([targetHost]), + }); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('returns non-proxy agents for non-matching proxyOnlyHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set([targetHost]), + }); + const { httpAgent, httpsAgent } = getCustomAgents( + configurationUtilities, + logger, + nonMatchingUrl + ); + expect(httpAgent instanceof HttpProxyAgent).toBeFalsy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts index 92ababf830aa7..ff2d005f4d841 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts @@ -19,7 +19,8 @@ interface GetCustomAgentsResponse { export function getCustomAgents( configurationUtilities: ActionsConfigurationUtilities, - logger: Logger + logger: Logger, + url: string ): GetCustomAgentsResponse { const proxySettings = configurationUtilities.getProxySettings(); const defaultAgents = { @@ -33,6 +34,28 @@ export function getCustomAgents( return defaultAgents; } + let targetUrl: URL; + try { + targetUrl = new URL(url); + } catch (err) { + logger.warn(`error determining proxy state for invalid url "${url}", using default agents`); + return defaultAgents; + } + + // filter out hostnames in the proxy bypass or only lists + const { hostname } = targetUrl; + + if (proxySettings.proxyBypassHosts) { + if (proxySettings.proxyBypassHosts.has(hostname)) { + return defaultAgents; + } + } + + if (proxySettings.proxyOnlyHosts) { + if (!proxySettings.proxyOnlyHosts.has(hostname)) { + return defaultAgents; + } + } logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`); let proxyUrl: URL; try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index cc3f03f50c36f..4b45c6d787cd6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -76,6 +76,8 @@ describe('send_email module', () => { { proxyUrl: 'https://example.com', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, } ); @@ -222,6 +224,138 @@ describe('send_email module', () => { await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops'); }); + + test('it bypasses with proxyBypassHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it does not bypass with proxyBypassHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://proxy.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it proxies with proxyOnlyHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://proxy.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it does not proxy with proxyOnlyHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); }); function getSendEmailOptions( diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index d4905015f7663..c0a254967b4fe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -63,6 +63,17 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom }; } + let useProxy = !!proxySettings; + + if (host) { + if (proxySettings?.proxyBypassHosts && proxySettings?.proxyBypassHosts?.has(host)) { + useProxy = false; + } + if (proxySettings?.proxyOnlyHosts && !proxySettings?.proxyOnlyHosts?.has(host)) { + useProxy = false; + } + } + if (service === JSON_TRANSPORT_SERVICE) { transportConfig.jsonTransport = true; delete transportConfig.auth; @@ -73,7 +84,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.port = port; transportConfig.secure = !!secure; - if (proxySettings) { + if (proxySettings && useProxy) { transportConfig.tls = { // do not fail on invalid certs if value is false rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates, diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 6479e29b5a76f..76612696e8e58 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -195,6 +195,8 @@ describe('execute()', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const actionTypeProxy = getActionType({ logger: mockedLogger, @@ -212,6 +214,106 @@ describe('execute()', () => { ); }); + test('ensure proxy bypass will bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy bypass will not bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy only will proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy only will not proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + test('renders parameter templates as expected', async () => { expect(actionType.renderParameterTemplates).toBeTruthy(); const paramsWithTemplates = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index a6173229e3267..d0fb4a8c4b935 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -7,6 +7,8 @@ import { URL } from 'url'; import { curry } from 'lodash'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; @@ -131,13 +133,15 @@ async function slackExecutor( const { message } = params; const proxySettings = configurationUtilities.getProxySettings(); - const customAgents = getCustomAgents(configurationUtilities, logger); + const customAgents = getCustomAgents(configurationUtilities, logger, webhookUrl); const agent = webhookUrl.toLowerCase().startsWith('https') ? customAgents.httpsAgent : customAgents.httpAgent; if (proxySettings) { - logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); + if (agent instanceof HttpProxyAgent || agent instanceof HttpsProxyAgent) { + logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); + } } try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index c31adddc5a57e..8a185d353de02 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -168,6 +168,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -230,6 +231,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index c468453247809..d3f059eede615 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -291,6 +291,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -329,6 +330,33 @@ describe('execute()', () => { `); }); + test('execute with exception maxContentLength size exceeded should log the proper error', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + hasAuth: true, + }; + requestMock.mockReset(); + requestMock.mockRejectedValueOnce({ + tag: 'err', + isAxiosError: true, + message: 'maxContentLength size of 1000000 exceeded', + }); + await actionType.executor({ + actionId: 'some-id', + services, + config, + secrets: { user: 'abc', password: '123' }, + params: { body: 'some data' }, + }); + expect(mockedLogger.error).toBeCalledWith( + 'error on some-id webhook event: maxContentLength size of 1000000 exceeded' + ); + }); + test('execute without username/password sends request without basic auth', async () => { const config: ActionTypeConfigType = { url: 'https://abc.def/my-webhook', @@ -355,6 +383,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 269449686acf0..93c9bbdbab18a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -180,7 +180,6 @@ export async function executor( return successResult(actionId, data); } else { const { error } = result; - if (error.response) { const { status, @@ -211,6 +210,10 @@ export async function executor( const message = `[${error.code}] ${error.message}`; logger.error(`error on ${actionId} webhook event: ${message}`); return errorResultRequestFailed(actionId, message); + } else if (error.isAxiosError) { + const message = `${error.message}`; + logger.error(`error on ${actionId} webhook event: ${message}`); + return errorResultRequestFailed(actionId, message); } logger.error(`error on ${actionId} webhook action: unexpected error`); diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index c90a5b2fb9768..2eecaa19da0c5 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -5,9 +5,17 @@ * 2.0. */ -import { configSchema } from './config'; +import { configSchema, ActionsConfig, getValidatedConfig } from './config'; +import { Logger } from '../../../..//src/core/server'; +import { loggingSystemMock } from '../../../..//src/core/server/mocks'; + +const mockLogger = loggingSystemMock.create().get() as jest.Mocked; describe('config validation', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + test('action defaults', () => { const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` @@ -19,9 +27,13 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, "preconfigured": Object {}, "proxyRejectUnauthorizedCertificates": true, "rejectUnauthorized": true, + "responseTimeout": "PT1M", } `); }); @@ -49,6 +61,9 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, "preconfigured": Object { "mySlack1": Object { "actionTypeId": ".slack", @@ -61,6 +76,7 @@ describe('config validation', () => { }, "proxyRejectUnauthorizedCertificates": false, "rejectUnauthorized": false, + "responseTimeout": "PT1M", } `); }); @@ -84,6 +100,56 @@ describe('config validation', () => { `"[preconfigured]: invalid preconfigured action id \\"__proto__\\""` ); }); + + test('validates proxyBypassHosts and proxyOnlyHosts', () => { + const bypassHosts = ['bypass.elastic.co']; + const onlyHosts = ['only.elastic.co']; + let validated: ActionsConfig; + + validated = configSchema.validate({}); + expect(validated.proxyBypassHosts).toEqual(undefined); + expect(validated.proxyOnlyHosts).toEqual(undefined); + + validated = configSchema.validate({ + proxyBypassHosts: bypassHosts, + }); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(undefined); + + validated = configSchema.validate({ + proxyOnlyHosts: onlyHosts, + }); + expect(validated.proxyBypassHosts).toEqual(undefined); + expect(validated.proxyOnlyHosts).toEqual(onlyHosts); + }); + + test('validates proxyBypassHosts and proxyOnlyHosts used at the same time', () => { + const bypassHosts = ['bypass.elastic.co']; + const onlyHosts = ['only.elastic.co']; + const config: Record = { + proxyBypassHosts: bypassHosts, + proxyOnlyHosts: onlyHosts, + }; + + let validated: ActionsConfig; + + // the config schema validation validates with both set + validated = configSchema.validate(config); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(onlyHosts); + + // getValidatedConfig will warn and set onlyHosts to undefined with both set + validated = getValidatedConfig(mockLogger, validated); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(undefined); + expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.", + ], + ] + `); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index b4f29b752957f..4aa77ded315b8 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -6,7 +6,15 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { AllowedHosts, EnabledActionTypes } from './actions_config'; +import { Logger } from '../../../../src/core/server'; + +export enum AllowedHosts { + Any = '*', +} + +export enum EnabledActionTypes { + Any = '*', +} const preconfiguredActionSchema = schema.object({ name: schema.string({ minLength: 1 }), @@ -36,11 +44,34 @@ export const configSchema = schema.object({ proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), + proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), + proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), rejectUnauthorized: schema.boolean({ defaultValue: true }), + maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }), + responseTimeout: schema.duration({ defaultValue: '60s' }), }); export type ActionsConfig = TypeOf; +// It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on +// simultaneous usage in the config validator directly, but there's no good way to express +// this relationship in the cloud config constraints, so we're doing it "live". +export function getValidatedConfig(logger: Logger, originalConfig: ActionsConfig): ActionsConfig { + const proxyBypassHosts = originalConfig.proxyBypassHosts; + const proxyOnlyHosts = originalConfig.proxyOnlyHosts; + + if (proxyBypassHosts && proxyOnlyHosts) { + logger.warn( + 'The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.' + ); + const tmp: Record = originalConfig; + delete tmp.proxyOnlyHosts; + return tmp as ActionsConfig; + } + + return originalConfig; +} + const invalidActionIds = new Set(['', '__proto__', 'constructor']); function validatePreconfigured(preconfigured: Record): string | undefined { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index b8f83e91239e2..30bbedbedbe9c 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment'; +import { ByteSizeValue } from '@kbn/config-schema'; import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; @@ -37,6 +39,8 @@ describe('Actions Plugin', () => { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -197,6 +201,8 @@ describe('Actions Plugin', () => { }, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5ec9241533b3c..bfe3b0a09ff2e 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -30,7 +30,7 @@ import { SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; -import { ActionsConfig } from './config'; +import { ActionsConfig, getValidatedConfig } from './config'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; @@ -141,8 +141,8 @@ export class ActionsPlugin implements Plugin(); this.logger = initContext.logger.get('actions'); + this.actionsConfig = getValidatedConfig(this.logger, initContext.config.get()); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; this.kibanaIndexConfig = initContext.config.legacy.get(); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 4e3916f5d6e23..b7a6750a520ea 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -133,6 +133,13 @@ export interface ActionTaskExecutorParams { export interface ProxySettings { proxyUrl: string; + proxyBypassHosts: Set | undefined; + proxyOnlyHosts: Set | undefined; proxyHeaders?: Record; proxyRejectUnauthorizedCertificates: boolean; } + +export interface ResponseSettings { + maxContentLength: number; + timeout: number; +} diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 10ebd09235236..d5c1105c99ad0 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 4010688746901..86ab00faeb5ad 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/apm/common/latency_aggregation_types.ts b/x-pack/plugins/apm/common/latency_aggregation_types.ts index d9db58f223144..964d6f4ed1015 100644 --- a/x-pack/plugins/apm/common/latency_aggregation_types.ts +++ b/x-pack/plugins/apm/common/latency_aggregation_types.ts @@ -14,7 +14,7 @@ export enum LatencyAggregationType { } export const latencyAggregationTypeRt = t.union([ - t.literal('avg'), - t.literal('p95'), - t.literal('p99'), + t.literal(LatencyAggregationType.avg), + t.literal(LatencyAggregationType.p95), + t.literal(LatencyAggregationType.p99), ]); diff --git a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts index 1a17f82a52141..970e39bc4f86f 100644 --- a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts +++ b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts @@ -21,8 +21,5 @@ export const isoToEpochRt = new t.Type( ? t.failure(input, context) : t.success(epochDate); }), - (a) => { - const d = new Date(a); - return d.toISOString(); - } + (output) => new Date(output).toISOString() ); diff --git a/x-pack/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json index 0c13dd717991c..c4587349c7ad7 100644 --- a/x-pack/plugins/apm/e2e/tsconfig.json +++ b/x-pack/plugins/apm/e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": ["tmp"], "include": ["./**/*"], "compilerOptions": { diff --git a/x-pack/plugins/apm/ftr_e2e/tsconfig.json b/x-pack/plugins/apm/ftr_e2e/tsconfig.json index f699943a254fa..168801f782607 100644 --- a/x-pack/plugins/apm/ftr_e2e/tsconfig.json +++ b/x-pack/plugins/apm/ftr_e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": [ "tmp" ], @@ -12,4 +12,4 @@ "node" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx index 619b4e3f3782a..8a54c76df0f69 100644 --- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, Axis, BarSeries, Chart, @@ -74,7 +74,7 @@ export function ChartPreview({ void) => { - const transaction = await callApmApi({ - signal: null, - endpoint: 'GET /api/apm/settings/custom_links/transaction', - params: { query: convertFiltersToQuery(filters) }, - }); - callback(transaction); - }, - 1000 -); - -const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); - -export function LinkPreview({ label, url, filters }: Props) { - const [transaction, setTransaction] = useState(); - - useEffect(() => { - /* - React throwns "Can't perform a React state update on an unmounted component" - It happens when the Custom Link flyout is closed before the return of the api request. - To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction. - */ - let isUnmounted = false; - fetchTransaction(filters, (_transaction: Transaction) => { - if (!isUnmounted) { - setTransaction(_transaction); - } - }); - return () => { - isUnmounted = true; - }; - }, [filters]); - - const { formattedUrl, error } = replaceTemplateVariables(url, transaction); - - return ( - - - {label - ? label - : i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.default.label', - { defaultMessage: 'Elastic.co' } - )} - - - - {url ? ( - - {formattedUrl} - - ) : ( - i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.default.url', - { defaultMessage: 'https://www.elastic.co' } - ) - )} - - - - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition', - { - defaultMessage: - 'Test your link with values from an example transaction document based on the filters above.', - } - )} - - - - - {error && ( - - - - )} - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index ccd2b0d425743..dfe768735d19b 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -22,7 +22,7 @@ import { FiltersSection } from './FiltersSection'; import { FlyoutFooter } from './FlyoutFooter'; import { LinkSection } from './LinkSection'; import { saveCustomLink } from './saveCustomLink'; -import { LinkPreview } from './LinkPreview'; +import { LinkPreview } from './link_preview'; import { Documentation } from './Documentation'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx new file mode 100644 index 0000000000000..3bf17a733bf8a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ComponentProps } from 'react'; +import { CoreStart } from 'kibana/public'; +import { createCallApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { LinkPreview } from './link_preview'; + +export default { + title: + 'app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview', + component: LinkPreview, +}; + +export function Example({ + filters, + label, + url, +}: ComponentProps) { + const coreMock = ({ + http: { + get: async () => ({ transaction: { id: '0' } }), + }, + uiSettings: { get: () => false }, + } as unknown) as CoreStart; + + createCallApmApi(coreMock); + + return ; +} +Example.args = { + filters: [], + label: 'Example label', + url: 'https://example.com', +} as ComponentProps; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx index 6348157104287..407f460f25ad3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview'; +import { LinkPreview } from '../CreateEditCustomLinkFlyout/link_preview'; import { render, getNodeText, @@ -14,15 +14,18 @@ import { act, waitFor, } from '@testing-library/react'; -import * as apmApi from '../../../../../../services/rest/createCallApmApi'; +import { + getCallApmApiSpy, + CallApmApiSpy, +} from '../../../../../../services/rest/callApmApiSpy'; export const removeExternalLinkText = (str: string) => str.replace(/\(opens in a new tab or window\)/g, ''); describe('LinkPreview', () => { - let callApmApiSpy: jest.SpyInstance; + let callApmApiSpy: CallApmApiSpy; beforeAll(() => { - callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({ + callApmApiSpy = getCallApmApiSpy().mockResolvedValue({ transaction: { id: 'foo' }, }); }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx new file mode 100644 index 0000000000000..726d4ba0d65ee --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiPanel, + EuiText, + EuiSpacer, + EuiLink, + EuiToolTip, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; +import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; + +export interface LinkPreviewProps { + label: string; + url: string; + filters: Filter[]; +} + +const fetchTransaction = debounce( + async (filters: Filter[], callback: (transaction: Transaction) => void) => { + const transaction = await callApmApi({ + signal: null, + endpoint: 'GET /api/apm/settings/custom_links/transaction', + params: { query: convertFiltersToQuery(filters) }, + }); + callback(transaction); + }, + 1000 +); + +const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); + +export function LinkPreview({ label, url, filters }: LinkPreviewProps) { + const [transaction, setTransaction] = useState(); + + useEffect(() => { + /* + React throwns "Can't perform a React state update on an unmounted component" + It happens when the Custom Link flyout is closed before the return of the api request. + To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction. + */ + let isUnmounted = false; + fetchTransaction(filters, (_transaction: Transaction) => { + if (!isUnmounted) { + setTransaction(_transaction); + } + }); + return () => { + isUnmounted = true; + }; + }, [filters]); + + const { formattedUrl, error } = replaceTemplateVariables(url, transaction); + + return ( + <> + +

    + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.previewSectionTitle', + { + defaultMessage: 'Preview', + } + )} +

    +
    + + + + {label + ? label + : i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.default.label', + { defaultMessage: 'Elastic.co' } + )} + + + + {url ? ( + + {formattedUrl} + + ) : ( + i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.default.url', + { defaultMessage: 'https://www.elastic.co' } + ) + )} + + + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition', + { + defaultMessage: + 'Test your link with values from an example transaction document based on the filters above.', + } + )} + + + + + {error && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 77835afef863a..7d119b8c406da 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -8,6 +8,7 @@ import { fireEvent, render, RenderResult } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; +import { getCallApmApiSpy } from '../../../../../services/rest/callApmApiSpy'; import { CustomLinkOverview } from '.'; import { License } from '../../../../../../../licensing/common/license'; import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context'; @@ -17,7 +18,6 @@ import { } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../../../context/license/license_context'; import * as hooks from '../../../../../hooks/use_fetcher'; -import * as apmApi from '../../../../../services/rest/createCallApmApi'; import { expectTextsInDocument, expectTextsNotInDocument, @@ -43,7 +43,7 @@ function getMockAPMContext({ canSave }: { canSave: boolean }) { describe('CustomLink', () => { beforeAll(() => { - jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); + getCallApmApiSpy().mockResolvedValue({}); }); afterAll(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 49fa3eab47862..ab18a31e76917 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -20,7 +20,7 @@ import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; import { useLicenseContext } from '../../../../../context/license/use_license_context'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; +import { LicensePrompt } from '../../../../shared/license_prompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; 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 72f0249f07bf6..62b39664cf63d 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 @@ -14,7 +14,7 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { LicensePrompt } from '../../../shared/LicensePrompt'; +import { LicensePrompt } from '../../../shared/license_prompt'; import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index e0651edbeb79b..62c547aa69e0d 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -34,7 +34,7 @@ import { } from '../../../../../observability/public'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useLicenseContext } from '../../../context/license/use_license_context'; -import { LicensePrompt } from '../../shared/LicensePrompt'; +import { LicensePrompt } from '../../shared/license_prompt'; import { IUrlParams } from '../../../context/url_params_context/types'; const latencyTab = { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index b30faac7a65af..c6ed4e640693f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -22,9 +22,12 @@ import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_b import { renderWithTheme } from '../../../utils/testHelpers'; import { ServiceOverview } from './'; import { waitFor } from '@testing-library/dom'; -import * as callApmApiModule from '../../../services/rest/createCallApmApi'; import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { + getCallApmApiSpy, + getCreateCallApmApiSpy, +} from '../../../services/rest/callApmApiSpy'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -83,10 +86,10 @@ describe('ServiceOverview', () => { /* eslint-disable @typescript-eslint/naming-convention */ const calls = { 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': { - error_groups: [], + error_groups: [] as any[], }, 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { - transactionGroups: [], + transactionGroups: [] as any[], totalTransactionGroups: 0, isAggregationAccurate: true, }, @@ -95,19 +98,17 @@ describe('ServiceOverview', () => { }; /* eslint-enable @typescript-eslint/naming-convention */ - jest - .spyOn(callApmApiModule, 'createCallApmApi') - .mockImplementation(() => {}); - - const callApmApi = jest - .spyOn(callApmApiModule, 'callApmApi') - .mockImplementation(({ endpoint }) => { + const callApmApiSpy = getCallApmApiSpy().mockImplementation( + ({ endpoint }) => { const response = calls[endpoint as keyof typeof calls]; return response ? Promise.resolve(response) : Promise.reject(`Response for ${endpoint} is not defined`); - }); + } + ); + + getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any); jest .spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown') .mockReturnValue({ @@ -124,7 +125,7 @@ describe('ServiceOverview', () => { ); await waitFor(() => - expect(callApmApi).toHaveBeenCalledTimes(Object.keys(calls).length) + expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length) ); expect((await findAllByText('Latency')).length).toBeGreaterThan(0); diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx deleted file mode 100644 index 97a48a61e47cc..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx +++ /dev/null @@ -1,63 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; - -interface Props { - text: string; - showBetaBadge?: boolean; -} - -export function LicensePrompt({ text, showBetaBadge = false }: Props) { - const licensePageUrl = useKibanaUrl( - '/app/management/stack/license_management' - ); - - const renderLicenseBody = ( - - {i18n.translate('xpack.apm.license.title', { - defaultMessage: 'Start free 30-day trial', - })} - - } - body={

    {text}

    } - actions={ - - {i18n.translate('xpack.apm.license.button', { - defaultMessage: 'Start trial', - })} - - } - /> - ); - - const renderWithBetaBadge = ( - - {renderLicenseBody} - - ); - - return <>{showBetaBadge ? renderWithBetaBadge : renderLicenseBody}; -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 2c9601d709cb4..f0faec4e99490 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, AreaSeries, Axis, Chart, @@ -139,7 +139,7 @@ export function TimeseriesChart({ {showAnnotations && ( ({ dataValue: annotation['@timestamp'], header: asAbsoluteDateTime(annotation['@timestamp']), diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 23016cc5dd8e9..436eca4781502 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, AreaSeries, Axis, Chart, @@ -102,7 +102,7 @@ export function TransactionBreakdownChartContents({ {showAnnotations && ( ({ dataValue: annotation['@timestamp'], header: asAbsoluteDateTime(annotation['@timestamp']), diff --git a/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx new file mode 100644 index 0000000000000..0950cff5127fc --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiCard, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; + +export interface LicensePromptProps { + text: string; + showBetaBadge?: boolean; +} + +export function LicensePrompt({ + text, + showBetaBadge = false, +}: LicensePromptProps) { + const licensePageUrl = useKibanaUrl( + '/app/management/stack/license_management' + ); + + return ( + {text}} + footer={ + + {i18n.translate('xpack.apm.license.button', { + defaultMessage: 'Start trial', + })} + + } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx rename to x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx index 57f782a020082..35e22b50306d9 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ComponentType } from 'react'; +import React, { ComponentProps, ComponentType } from 'react'; import { LicensePrompt } from '.'; import { ApmPluginContext, @@ -17,19 +17,25 @@ const contextMock = ({ } as unknown) as ApmPluginContextValue; export default { - title: 'app/LicensePrompt', + title: 'shared/LicensePrompt', component: LicensePrompt, decorators: [ (Story: ComponentType) => ( - {' '} + ), ], }; -export function Example() { - return ( - - ); +export function Example({ + showBetaBadge, + text, +}: ComponentProps) { + return ; } +Example.args = { + showBetaBadge: false, + text: + 'To create Feature name, you must be subscribed to an Elastic X license or above.', +} as ComponentProps; diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 1821e92ee5a78..00447607cf787 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -10,10 +10,10 @@ import { fetchObservabilityOverviewPageData, getHasData, } from './apm_observability_overview_fetchers'; -import * as createCallApmApi from './createCallApmApi'; +import { getCallApmApiSpy } from './callApmApiSpy'; describe('Observability dashboard data', () => { - const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); + const callApmApiMock = getCallApmApiSpy(); const params = { absoluteTime: { start: moment('2020-07-02T13:25:11.629Z').valueOf(), @@ -46,11 +46,14 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 10, - transactionCoordinates: [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 3 }, - ], + transactionPerMinute: { + value: 2, + timeseries: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -81,7 +84,7 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [], + transactionPerMinute: { value: null, timeseries: [] as any }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -108,7 +111,10 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + transactionPerMinute: { + value: 0, + timeseries: [{ x: 1 }, { x: 2 }, { x: 3 }], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 55ead8d942aca..3a02efd05e5a5 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { mean } from 'lodash'; import { ApmFetchDataResponse, FetchDataParams, @@ -31,7 +30,7 @@ export const fetchObservabilityOverviewPageData = async ({ }, }); - const { serviceCount, transactionCoordinates } = data; + const { serviceCount, transactionPerMinute } = data; return { appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, @@ -42,17 +41,12 @@ export const fetchObservabilityOverviewPageData = async ({ }, transactions: { type: 'number', - value: - mean( - transactionCoordinates - .map(({ y }) => y) - .filter((y) => y && isFinite(y)) - ) || 0, + value: transactionPerMinute.value || 0, }, }, series: { transactions: { - coordinates: transactionCoordinates, + coordinates: transactionPerMinute.timeseries, }, }, }; diff --git a/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts new file mode 100644 index 0000000000000..ba9f740e06d0d --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as createCallApmApi from './createCallApmApi'; +import type { AbstractAPMClient } from './createCallApmApi'; + +export type CallApmApiSpy = jest.SpyInstance< + Promise, + Parameters +>; + +export type CreateCallApmApiSpy = jest.SpyInstance; + +export const getCreateCallApmApiSpy = () => + (jest.spyOn( + createCallApmApi, + 'createCallApmApi' + ) as unknown) as CreateCallApmApiSpy; +export const getCallApmApiSpy = () => + (jest.spyOn(createCallApmApi, 'callApmApi') as unknown) as CallApmApiSpy; diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index b0cce3296fe21..0e82d70faf1e1 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -6,30 +6,68 @@ */ import { CoreSetup, CoreStart } from 'kibana/public'; -import { parseEndpoint } from '../../../common/apm_api/parse_endpoint'; +import * as t from 'io-ts'; +import type { + ClientRequestParamsOf, + EndpointOf, + ReturnOf, + RouteRepositoryClient, + ServerRouteRepository, + ServerRoute, +} from '@kbn/server-route-repository'; +import { formatRequest } from '@kbn/server-route-repository/target/format_request'; import { FetchOptions } from '../../../common/fetch_options'; import { callApi } from './callApi'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { APMAPI } from '../../../server/routes/create_apm_api'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { Client } from '../../../server/routes/typings'; - -export type APMClient = Client; -export type AutoAbortedAPMClient = Client; +import type { + APMServerRouteRepository, + InspectResponse, + APMRouteHandlerResources, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server'; export type APMClientOptions = Omit< FetchOptions, 'query' | 'body' | 'pathname' | 'signal' > & { - endpoint: string; signal: AbortSignal | null; - params?: { - body?: any; - query?: Record; - path?: Record; - }; }; +export type APMClient = RouteRepositoryClient< + APMServerRouteRepository, + APMClientOptions +>; + +export type AutoAbortedAPMClient = RouteRepositoryClient< + APMServerRouteRepository, + Omit +>; + +export type APIReturnType< + TEndpoint extends EndpointOf +> = ReturnOf & { + _inspect?: InspectResponse; +}; + +export type APIEndpoint = EndpointOf; + +export type APIClientRequestParamsOf< + TEndpoint extends EndpointOf +> = ClientRequestParamsOf; + +export type AbstractAPMRepository = ServerRouteRepository< + APMRouteHandlerResources, + {}, + Record< + string, + ServerRoute + > +>; + +export type AbstractAPMClient = RouteRepositoryClient< + AbstractAPMRepository, + APMClientOptions +>; + export let callApmApi: APMClient = () => { throw new Error( 'callApmApi has to be initialized before used. Call createCallApmApi first.' @@ -37,9 +75,13 @@ export let callApmApi: APMClient = () => { }; export function createCallApmApi(core: CoreStart | CoreSetup) { - callApmApi = ((options: APMClientOptions) => { - const { endpoint, params, ...opts } = options; - const { method, pathname } = parseEndpoint(endpoint, params?.path); + callApmApi = ((options) => { + const { endpoint, ...opts } = options; + const { params } = (options as unknown) as { + params?: Partial>; + }; + + const { method, pathname } = formatRequest(endpoint, params?.path); return callApi(core, { ...opts, @@ -50,10 +92,3 @@ export function createCallApmApi(core: CoreStart | CoreSetup) { }); }) as APMClient; } - -// infer return type from API -export type APIReturnType< - TPath extends keyof APMAPI['_S'] -> = APMAPI['_S'][TPath] extends { ret: any } - ? APMAPI['_S'][TPath]['ret'] - : unknown; diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index 613435fe186bf..58fb096ca3a51 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -18,11 +18,16 @@ const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const unlink = promisify(fs.unlink); -const { kibanaRoot, tsconfigTpl, filesToIgnore } = require('./paths'); +const { + kibanaRoot, + tsconfigTpl, + tsconfigTplTest, + filesToIgnore, +} = require('./paths'); const { unoptimizeTsConfig } = require('./unoptimize'); async function prepareBaseTsConfig() { - const baseConfigFilename = path.resolve(kibanaRoot, 'tsconfig.project.json'); + const baseConfigFilename = path.resolve(kibanaRoot, 'tsconfig.base.json'); const config = json5.parse(await readFile(baseConfigFilename, 'utf-8')); await writeFile( @@ -57,6 +62,23 @@ async function addApmFilesToRootTsConfig() { ); } +async function addApmFilesToTestTsConfig() { + const template = json5.parse(await readFile(tsconfigTplTest, 'utf-8')); + const testTsConfigFilename = path.join( + kibanaRoot, + 'x-pack/test/tsconfig.json' + ); + const testTsConfig = json5.parse( + await readFile(testTsConfigFilename, 'utf-8') + ); + + await writeFile( + testTsConfigFilename, + JSON.stringify({ ...testTsConfig, ...template, references: [] }, null, 2), + { encoding: 'utf-8' } + ); +} + async function setIgnoreChanges() { for (const filename of filesToIgnore) { await execa('git', ['update-index', '--skip-worktree', filename]); @@ -74,6 +96,8 @@ async function optimizeTsConfig() { await addApmFilesToRootTsConfig(); + await addApmFilesToTestTsConfig(); + await deleteApmTsConfig(); await setIgnoreChanges(); diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index b501ec3a8eedf..bde129f434934 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -9,15 +9,18 @@ const path = require('path'); const kibanaRoot = path.resolve(__dirname, '../../../../..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); +const tsconfigTplTest = path.resolve(__dirname, './test-tsconfig.json'); const filesToIgnore = [ path.resolve(kibanaRoot, 'tsconfig.json'), - path.resolve(kibanaRoot, 'tsconfig.project.json'), + path.resolve(kibanaRoot, 'tsconfig.base.json'), path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json'), + path.resolve(kibanaRoot, 'x-pack/test', 'tsconfig.json'), ]; module.exports = { kibanaRoot, tsconfigTpl, + tsconfigTplTest, filesToIgnore, }; diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json new file mode 100644 index 0000000000000..d6718b7511179 --- /dev/null +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "types": [ + "node" + ], + "noErrorTruncation": true + }, + "include": [ + "./apm_api_integration/**/*", + "../../packages/kbn-test/types/**/*", + "../../typings/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index c7102ce913f01..695a9ba70f5d7 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -23,6 +23,8 @@ const tsconfig = useOptimizedTsConfig ? resolve(root, 'tsconfig.json') : resolve(root, 'x-pack/plugins/apm/tsconfig.json'); +const testTsconfig = resolve(root, 'x-pack/test/tsconfig.json'); + const tasks = new Listr( [ { @@ -55,16 +57,18 @@ const tasks = new Listr( ], execaOpts ).then(() => - execa( - require.resolve('typescript/bin/tsc'), - [ - '--project', - tsconfig, - '--pretty', - ...(useOptimizedTsConfig ? ['--noEmit'] : []), - ], - execaOpts - ) + Promise.all([ + execa( + require.resolve('typescript/bin/tsc'), + ['--project', tsconfig, '--pretty', '--noEmit'], + execaOpts + ), + execa( + require.resolve('typescript/bin/tsc'), + ['--project', testTsconfig, '--pretty', '--noEmit'], + execaOpts + ), + ]) ), }, { diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 00910353ac278..9ab56c1a303ea 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -120,5 +120,9 @@ export function mergeConfigs( export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); -export { APMPlugin, APMPluginSetup } from './plugin'; +export { APMPlugin } from './plugin'; +export { APMPluginSetup } from './types'; +export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; +export { InspectResponse, APMRouteHandlerResources } from './routes/typings'; + export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 1f0aa401bcab0..989297544c78f 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -10,7 +10,7 @@ import { omit } from 'lodash'; import chalk from 'chalk'; import { KibanaRequest } from '../../../../../../../src/core/server'; -import { inspectableEsQueriesMap } from '../../../routes/create_api'; +import { inspectableEsQueriesMap } from '../../../routes/register_routes'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 45e17c1678518..9d7434d127ead 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { KibanaRequest } from 'src/core/server'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { CreateIndexRequest, @@ -13,7 +12,7 @@ import { IndexRequest, } from '@elastic/elasticsearch/api/types'; import { unwrapEsResponse } from '../../../../../../observability/server'; -import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { APMRouteHandlerResources } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, @@ -31,11 +30,9 @@ export type APMInternalClient = ReturnType; export function createInternalESClient({ context, + debug, request, -}: { - context: APMRequestHandlerContext; - request: KibanaRequest; -}) { +}: Pick & { debug: boolean }) { const { asInternalUser } = context.core.elasticsearch.client; function callEs({ @@ -53,7 +50,7 @@ export function createInternalESClient({ title: getDebugTitle(request), body: getDebugBody(params, requestType), }), - debug: context.params.query._inspect, + debug, isCalledWithInternalUser: true, request, requestType, diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index c0707d0286180..c0ff0cab88f47 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,8 +7,7 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { KibanaRequest } from '../../../../../../src/core/server'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { ProcessorEvent } from '../../../common/processor_event'; import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; @@ -32,7 +31,7 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({ }, })); -function getMockRequest() { +function getMockResources() { const esClientMock = { asCurrentUser: { search: jest.fn().mockResolvedValue({ body: {} }), @@ -42,7 +41,7 @@ function getMockRequest() { }, }; - const mockContext = ({ + const mockResources = ({ config: new Proxy( {}, { @@ -54,65 +53,69 @@ function getMockRequest() { _inspect: false, }, }, - core: { - elasticsearch: { - client: esClientMock, - }, - uiSettings: { - client: { - get: jest.fn().mockResolvedValue(false), + context: { + core: { + elasticsearch: { + client: esClientMock, }, - }, - savedObjects: { - client: { - get: jest.fn(), + uiSettings: { + client: { + get: jest.fn().mockResolvedValue(false), + }, + }, + savedObjects: { + client: { + get: jest.fn(), + }, }, }, }, plugins: { ml: undefined, }, - } as unknown) as APMRequestHandlerContext & { - core: { - elasticsearch: { - client: typeof esClientMock; - }; - uiSettings: { - client: { - get: jest.Mock; + request: { + url: '', + events: { + aborted$: { + subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), + }, + }, + }, + } as unknown) as APMRouteHandlerResources & { + context: { + core: { + elasticsearch: { + client: typeof esClientMock; }; - }; - savedObjects: { - client: { - get: jest.Mock; + uiSettings: { + client: { + get: jest.Mock; + }; + }; + savedObjects: { + client: { + get: jest.Mock; + }; }; }; }; }; - const mockRequest = ({ - url: '', - events: { - aborted$: { - subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), - }, - }, - } as unknown) as KibanaRequest; - - return { mockContext, mockRequest }; + return mockResources; } describe('setupRequest', () => { describe('with default args', () => { it('calls callWithRequest', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [ProcessorEvent.transaction] }, body: { foo: 'bar' }, }); + expect( - mockContext.core.elasticsearch.client.asCurrentUser.search + mockResources.context.core.elasticsearch.client.asCurrentUser.search ).toHaveBeenCalledWith({ index: ['apm-*'], body: { @@ -132,14 +135,14 @@ describe('setupRequest', () => { }); it('calls callWithInternalUser', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { internalClient } = await setupRequest(mockResources); await internalClient.search({ index: ['apm-*'], body: { foo: 'bar' }, } as any); expect( - mockContext.core.elasticsearch.client.asInternalUser.search + mockResources.context.core.elasticsearch.client.asInternalUser.search ).toHaveBeenCalledWith({ index: ['apm-*'], body: { @@ -151,8 +154,8 @@ describe('setupRequest', () => { describe('with a bool filter', () => { it('adds a range filter for `observer.version_major` to the existing filter', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [ProcessorEvent.transaction], @@ -162,8 +165,8 @@ describe('setupRequest', () => { }, }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock - .calls[0][0]; + mockResources.context.core.elasticsearch.client.asCurrentUser.search + .mock.calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -178,8 +181,8 @@ describe('setupRequest', () => { }); it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search( { apm: { @@ -194,8 +197,8 @@ describe('setupRequest', () => { } ); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock - .calls[0][0]; + mockResources.context.core.elasticsearch.client.asCurrentUser.search + .mock.calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -216,15 +219,15 @@ describe('setupRequest', () => { describe('without a bool filter', () => { it('adds a range filter for `observer.version_major`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const mockResources = getMockResources(); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [ProcessorEvent.error], }, }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock + mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; expect(params.body).toEqual({ query: { @@ -241,12 +244,12 @@ describe('without a bool filter', () => { describe('with includeFrozen=false', () => { it('sets `ignore_throttled=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const mockResources = getMockResources(); // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); + mockResources.context.core.uiSettings.client.get.mockResolvedValue(false); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { @@ -255,7 +258,7 @@ describe('with includeFrozen=false', () => { }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock + mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; expect(params.ignore_throttled).toBe(true); }); @@ -263,19 +266,19 @@ describe('with includeFrozen=false', () => { describe('with includeFrozen=true', () => { it('sets `ignore_throttled=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const mockResources = getMockResources(); // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); + mockResources.context.core.uiSettings.client.get.mockResolvedValue(true); - const { apmEventClient } = await setupRequest(mockContext, mockRequest); + const { apmEventClient } = await setupRequest(mockResources); await apmEventClient.search({ apm: { events: [] }, }); const params = - mockContext.core.elasticsearch.client.asCurrentUser.search.mock + mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; expect(params.ignore_throttled).toBe(false); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index fff661250c6df..40836cb6635e3 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -11,7 +11,7 @@ import { APMConfig } from '../..'; import { KibanaRequest } from '../../../../../../src/core/server'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { UIFilters } from '../../../typings/ui_filters'; -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { ApmIndicesConfig, getApmIndices, @@ -44,7 +44,7 @@ export interface SetupTimeRange { } interface SetupRequestParams { - query?: { + query: { _inspect?: boolean; /** @@ -64,13 +64,19 @@ type InferSetup = Setup & (TParams extends { query: { start: number } } ? { start: number } : {}) & (TParams extends { query: { end: number } } ? { end: number } : {}); -export async function setupRequest( - context: APMRequestHandlerContext, - request: KibanaRequest -): Promise> { +export async function setupRequest({ + context, + params, + core, + plugins, + request, + config, + logger, +}: APMRouteHandlerResources & { + params: TParams; +}): Promise> { return withApmSpan('setup_request', async () => { - const { config, logger } = context; - const { query } = context.params; + const { query } = params; const [indices, includeFrozen] = await Promise.all([ getApmIndices({ @@ -88,7 +94,7 @@ export async function setupRequest( indices, apmEventClient: createApmEventClient({ esClient: context.core.elasticsearch.client.asCurrentUser, - debug: context.params.query._inspect, + debug: query._inspect, request, indices, options: { includeFrozen }, @@ -96,11 +102,12 @@ export async function setupRequest( internalClient: createInternalESClient({ context, request, + debug: query._inspect, }), ml: - context.plugins.ml && isActivePlatinumLicense(context.licensing.license) + plugins.ml && isActivePlatinumLicense(context.licensing.license) ? getMlSetup( - context.plugins.ml, + plugins.ml.setup, context.core.savedObjects.client, request ) @@ -118,8 +125,8 @@ export async function setupRequest( } function getMlSetup( - ml: Required['ml'], - savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'], + ml: Required['ml']['setup'], + savedObjectsClient: APMRouteHandlerResources['context']['core']['savedObjects']['client'], request: KibanaRequest ) { return { diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts index 19163da449b90..a5340c1220b44 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts @@ -8,21 +8,9 @@ import { createStaticIndexPattern } from './create_static_index_pattern'; import { Setup } from '../helpers/setup_request'; import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data'; -import { APMRequestHandlerContext } from '../../routes/typings'; import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client'; +import { APMConfig } from '../..'; -function getMockContext(config: Record) { - return ({ - config, - core: { - savedObjects: { - client: { - create: jest.fn(), - }, - }, - }, - } as unknown) as APMRequestHandlerContext; -} function getMockSavedObjectsClient() { return ({ create: jest.fn(), @@ -32,13 +20,13 @@ function getMockSavedObjectsClient() { describe('createStaticIndexPattern', () => { it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => { const setup = {} as Setup; - const context = getMockContext({ - 'xpack.apm.autocreateApmIndexPattern': false, - }); + const savedObjectsClient = getMockSavedObjectsClient(); await createStaticIndexPattern( setup, - context, + { + 'xpack.apm.autocreateApmIndexPattern': false, + } as APMConfig, savedObjectsClient, 'default' ); @@ -47,9 +35,6 @@ describe('createStaticIndexPattern', () => { it(`should not create index pattern if no APM data is found`, async () => { const setup = {} as Setup; - const context = getMockContext({ - 'xpack.apm.autocreateApmIndexPattern': true, - }); // does not have APM data jest @@ -60,7 +45,9 @@ describe('createStaticIndexPattern', () => { await createStaticIndexPattern( setup, - context, + { + 'xpack.apm.autocreateApmIndexPattern': true, + } as APMConfig, savedObjectsClient, 'default' ); @@ -69,9 +56,6 @@ describe('createStaticIndexPattern', () => { it(`should create index pattern`, async () => { const setup = {} as Setup; - const context = getMockContext({ - 'xpack.apm.autocreateApmIndexPattern': true, - }); // does have APM data jest @@ -82,7 +66,9 @@ describe('createStaticIndexPattern', () => { await createStaticIndexPattern( setup, - context, + { + 'xpack.apm.autocreateApmIndexPattern': true, + } as APMConfig, savedObjectsClient, 'default' ); diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index b91fb8342a212..e627e9ed1d6cf 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -12,20 +12,18 @@ import { } from '../../../../../../src/plugins/apm_oss/server'; import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data'; import { Setup } from '../helpers/setup_request'; -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client.js'; import { withApmSpan } from '../../utils/with_apm_span'; import { getApmIndexPatternTitle } from './get_apm_index_pattern_title'; export async function createStaticIndexPattern( setup: Setup, - context: APMRequestHandlerContext, + config: APMRouteHandlerResources['config'], savedObjectsClient: InternalSavedObjectsClient, spaceId: string | undefined ): Promise { return withApmSpan('create_static_index_pattern', async () => { - const { config } = context; - // don't autocreate APM index pattern if it's been disabled via the config if (!config['xpack.apm.autocreateApmIndexPattern']) { return false; @@ -39,7 +37,7 @@ export async function createStaticIndexPattern( } try { - const apmIndexPatternTitle = getApmIndexPatternTitle(context); + const apmIndexPatternTitle = getApmIndexPatternTitle(config); await withApmSpan('create_index_pattern_saved_object', () => savedObjectsClient.create( 'index-pattern', diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts index 41abe82de8ff2..faec64c798c7d 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; -export function getApmIndexPatternTitle(context: APMRequestHandlerContext) { - return context.config['apm_oss.indexPattern']; +export function getApmIndexPatternTitle( + config: APMRouteHandlerResources['config'] +) { + return config['apm_oss.indexPattern']; } diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 8b81101fd2f39..8bbc22fbf289d 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -5,12 +5,11 @@ * 2.0. */ -import LRU from 'lru-cache'; import { IndexPatternsFetcher, FieldDescriptor, } from '../../../../../../src/plugins/data/server'; -import { APMRequestHandlerContext } from '../../routes/typings'; +import { APMRouteHandlerResources } from '../../routes/typings'; import { withApmSpan } from '../../utils/with_apm_span'; export interface IndexPatternTitleAndFields { @@ -19,24 +18,14 @@ export interface IndexPatternTitleAndFields { fields: FieldDescriptor[]; } -const cache = new LRU({ - max: 100, - maxAge: 1000 * 60, -}); - // TODO: this is currently cached globally. In the future we might want to cache this per user export const getDynamicIndexPattern = ({ + config, context, -}: { - context: APMRequestHandlerContext; -}) => { + logger, +}: Pick) => { return withApmSpan('get_dynamic_index_pattern', async () => { - const indexPatternTitle = context.config['apm_oss.indexPattern']; - - const CACHE_KEY = `apm_dynamic_index_pattern_${indexPatternTitle}`; - if (cache.has(CACHE_KEY)) { - return cache.get(CACHE_KEY); - } + const indexPatternTitle = config['apm_oss.indexPattern']; const indexPatternsFetcher = new IndexPatternsFetcher( context.core.elasticsearch.client.asCurrentUser @@ -57,14 +46,11 @@ export const getDynamicIndexPattern = ({ title: indexPatternTitle, }; - cache.set(CACHE_KEY, indexPattern); return indexPattern; } catch (e) { - // since `getDynamicIndexPattern` can be called multiple times per request it can be expensive not to cache failed lookups - cache.set(CACHE_KEY, undefined); const notExists = e.output?.statusCode === 404; if (notExists) { - context.logger.error( + logger.error( `Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist` ); return; diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts deleted file mode 100644 index aac18e2bdfe4c..0000000000000 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { rangeQuery } from '../../../server/utils/queries'; -import { Coordinates } from '../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { calculateThroughput } from '../helpers/calculate_throughput'; -import { withApmSpan } from '../../utils/with_apm_span'; - -export function getTransactionCoordinates({ - setup, - bucketSize, - searchAggregatedTransactions, -}: { - setup: Setup & SetupTimeRange; - bucketSize: string; - searchAggregatedTransactions: boolean; -}): Promise { - return withApmSpan( - 'observability_overview_get_transaction_distribution', - async () => { - const { apmEventClient, start, end } = setup; - - const { aggregations } = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), - }, - }, - aggs: { - distribution: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - min_doc_count: 0, - }, - }, - }, - }, - }); - - return ( - aggregations?.distribution.buckets.map((bucket) => ({ - x: bucket.key, - y: calculateThroughput({ start, end, value: bucket.doc_count }), - })) || [] - ); - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts new file mode 100644 index 0000000000000..da8ac7c50b594 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { rangeQuery } from '../../../server/utils/queries'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { calculateThroughput } from '../helpers/calculate_throughput'; +import { withApmSpan } from '../../utils/with_apm_span'; + +export function getTransactionsPerMinute({ + setup, + bucketSize, + searchAggregatedTransactions, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; + searchAggregatedTransactions: boolean; +}) { + return withApmSpan( + 'observability_overview_get_transactions_per_minute', + async () => { + const { apmEventClient, start, end } = setup; + + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), + }, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, + }, + }, + }, + }, + }, + }); + + if (!aggregations || !aggregations.transactionType.buckets) { + return { value: undefined, timeseries: [] }; + } + + const topTransactionTypeBucket = + aggregations.transactionType.buckets.find( + ({ key: transactionType }) => + transactionType === TRANSACTION_REQUEST || + transactionType === TRANSACTION_PAGE_LOAD + ) || aggregations.transactionType.buckets[0]; + + return { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket?.doc_count || 0, + }), + timeseries: + topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.throughput.value, + })) || [], + }; + } + ); +} diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index a1587611b0a2a..d8dbc242986a6 100644 --- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -14,7 +14,7 @@ import { APM_INDICES_SAVED_OBJECT_ID, } from '../../../../common/apm_saved_object_constants'; import { APMConfig } from '../../..'; -import { APMRequestHandlerContext } from '../../../routes/typings'; +import { APMRouteHandlerResources } from '../../../routes/typings'; import { withApmSpan } from '../../../utils/with_apm_span'; type ISavedObjectsClient = Pick; @@ -91,9 +91,8 @@ const APM_UI_INDICES: ApmIndicesName[] = [ export async function getApmIndexSettings({ context, -}: { - context: APMRequestHandlerContext; -}) { + config, +}: Pick) { let apmIndicesSavedObject: PromiseReturnType; try { apmIndicesSavedObject = await getApmIndicesSavedObject( @@ -106,7 +105,7 @@ export async function getApmIndexSettings({ throw error; } } - const apmIndicesConfig = getApmIndicesConfig(context.config); + const apmIndicesConfig = getApmIndicesConfig(config); return APM_UI_INDICES.map((configurationName) => ({ configurationName, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index db96794627519..074df7eaafd3c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { combineLatest, Observable } from 'rxjs'; +import { combineLatest } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { CoreSetup, @@ -16,22 +16,10 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; -import { SpacesPluginSetup } from '../../spaces/server'; +import { mapValues } from 'lodash'; import { APMConfig, APMXPackConfig } from '.'; import { mergeConfigs } from './index'; -import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; -import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { UI_SETTINGS } from '../../../../src/plugins/data/common'; -import { ActionsPlugin } from '../../actions/server'; -import { AlertingPlugin } from '../../alerting/server'; -import { CloudSetup } from '../../cloud/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { LicensingPluginSetup } from '../../licensing/server'; -import { MlPluginSetup } from '../../ml/server'; -import { ObservabilityPluginSetup } from '../../observability/server'; -import { SecurityPluginSetup } from '../../security/server'; -import { TaskManagerSetupContract } from '../../task_manager/server'; import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; @@ -40,23 +28,29 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_ import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; -import { createApmApi } from './routes/create_apm_api'; import { apmIndices, apmTelemetry } from './saved_objects'; import { createElasticCloudInstructions } from './tutorial/elastic_cloud'; import { uiSettings } from './ui_settings'; -import type { ApmPluginRequestHandlerContext } from './routes/typings'; - -export interface APMPluginSetup { - config$: Observable; - getApmIndices: () => ReturnType; - createApmEventClient: (params: { - debug?: boolean; - request: KibanaRequest; - context: ApmPluginRequestHandlerContext; - }) => Promise>; -} - -export class APMPlugin implements Plugin { +import type { + ApmPluginRequestHandlerContext, + APMRouteHandlerResources, +} from './routes/typings'; +import { + APMPluginSetup, + APMPluginSetupDependencies, + APMPluginStartDependencies, +} from './types'; +import { registerRoutes } from './routes/register_routes'; +import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository'; + +export class APMPlugin + implements + Plugin< + APMPluginSetup, + void, + APMPluginSetupDependencies, + APMPluginStartDependencies + > { private currentConfig?: APMConfig; private logger?: Logger; constructor(private readonly initContext: PluginInitializerContext) { @@ -64,22 +58,8 @@ export class APMPlugin implements Plugin { } public setup( - core: CoreSetup, - plugins: { - spaces?: SpacesPluginSetup; - apmOss: APMOSSPluginSetup; - home: HomeServerPluginSetup; - licensing: LicensingPluginSetup; - cloud?: CloudSetup; - usageCollection?: UsageCollectionSetup; - taskManager?: TaskManagerSetupContract; - alerting?: AlertingPlugin['setup']; - actions?: ActionsPlugin['setup']; - observability?: ObservabilityPluginSetup; - features: FeaturesPluginSetup; - security?: SecurityPluginSetup; - ml?: MlPluginSetup; - } + core: CoreSetup, + plugins: Omit ) { this.logger = this.initContext.logger.get(); const config$ = this.initContext.config.create(); @@ -101,11 +81,13 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = mergeConfigs( + const currentConfig = mergeConfigs( plugins.apmOss.config, this.initContext.config.get() ); + this.currentConfig = currentConfig; + if ( plugins.taskManager && plugins.usageCollection && @@ -122,8 +104,8 @@ export class APMPlugin implements Plugin { } const ossTutorialProvider = plugins.apmOss.getRegisteredTutorialProvider(); - plugins.home.tutorials.unregisterTutorial(ossTutorialProvider); - plugins.home.tutorials.registerTutorial(() => { + plugins.home?.tutorials.unregisterTutorial(ossTutorialProvider); + plugins.home?.tutorials.registerTutorial(() => { const ossPart = ossTutorialProvider({}); if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) { ossPart.artifacts.application = { @@ -147,10 +129,26 @@ export class APMPlugin implements Plugin { registerFeaturesUsage({ licensingPlugin: plugins.licensing }); - createApmApi().init(core, { - config$: mergedConfig$, - logger: this.logger!, - plugins, + registerRoutes({ + core: { + setup: core, + start: () => core.getStartServices().then(([coreStart]) => coreStart), + }, + logger: this.logger, + config: currentConfig, + repository: getGlobalApmServerRouteRepository(), + plugins: mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return pluginsStartContracts[ + key as keyof APMPluginStartDependencies + ]; + }), + }; + }) as APMRouteHandlerResources['plugins'], }); const boundGetApmIndices = async () => diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts index 3bebcd49ec34a..0175860e93d35 100644 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts @@ -10,7 +10,8 @@ import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_previ import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count'; import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate'; import { setupRequest } from '../../lib/helpers/setup_request'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; import { rangeRt } from '../default_api_types'; const alertParamsRt = t.intersection([ @@ -29,13 +30,14 @@ const alertParamsRt = t.intersection([ export type AlertParams = t.TypeOf; -export const transactionErrorRateChartPreview = createRoute({ +const transactionErrorRateChartPreview = createApmServerRoute({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', params: t.type({ query: alertParamsRt }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _inspect, ...alertParams } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { _inspect, ...alertParams } = params.query; const errorRateChartPreview = await getTransactionErrorRateChartPreview({ setup, @@ -46,13 +48,16 @@ export const transactionErrorRateChartPreview = createRoute({ }, }); -export const transactionErrorCountChartPreview = createRoute({ +const transactionErrorCountChartPreview = createApmServerRoute({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', params: t.type({ query: alertParamsRt }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _inspect, ...alertParams } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { _inspect, ...alertParams } = params.query; + const errorCountChartPreview = await getTransactionErrorCountChartPreview({ setup, alertParams, @@ -62,13 +67,16 @@ export const transactionErrorCountChartPreview = createRoute({ }, }); -export const transactionDurationChartPreview = createRoute({ +const transactionDurationChartPreview = createApmServerRoute({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', params: t.type({ query: alertParamsRt }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _inspect, ...alertParams } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params } = resources; + + const { _inspect, ...alertParams } = params.query; const latencyChartPreview = await getTransactionDurationChartPreview({ alertParams, @@ -78,3 +86,9 @@ export const transactionDurationChartPreview = createRoute({ return { latencyChartPreview }; }, }); + +export const alertsChartPreviewRouteRepository = createApmServerRouteRepository() + .add(transactionErrorRateChartPreview) + .add(transactionDurationChartPreview) + .add(transactionErrorCountChartPreview) + .add(transactionDurationChartPreview); diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index c7c69e0774822..4728aa2e8d3f6 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -14,7 +14,8 @@ import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overal import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions'; import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; const INVALID_LICENSE = i18n.translate( @@ -25,7 +26,7 @@ const INVALID_LICENSE = i18n.translate( } ); -export const correlationsLatencyDistributionRoute = createRoute({ +const correlationsLatencyDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/latency/overall_distribution', params: t.type({ query: t.intersection([ @@ -40,18 +41,19 @@ export const correlationsLatencyDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, serviceName, transactionType, transactionName, - } = context.params.query; + } = params.query; return getOverallLatencyDistribution({ environment, @@ -64,7 +66,7 @@ export const correlationsLatencyDistributionRoute = createRoute({ }, }); -export const correlationsForSlowTransactionsRoute = createRoute({ +const correlationsForSlowTransactionsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/latency/slow_transactions', params: t.type({ query: t.intersection([ @@ -85,11 +87,13 @@ export const correlationsForSlowTransactionsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, @@ -100,7 +104,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ fieldNames, maxLatency, distributionInterval, - } = context.params.query; + } = params.query; return getCorrelationsForSlowTransactions({ environment, @@ -117,7 +121,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ }, }); -export const correlationsErrorDistributionRoute = createRoute({ +const correlationsErrorDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', params: t.type({ query: t.intersection([ @@ -132,18 +136,20 @@ export const correlationsErrorDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { params, context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, serviceName, transactionType, transactionName, - } = context.params.query; + } = params.query; return getOverallErrorTimeseries({ environment, @@ -156,7 +162,7 @@ export const correlationsErrorDistributionRoute = createRoute({ }, }); -export const correlationsForFailedTransactionsRoute = createRoute({ +const correlationsForFailedTransactionsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/correlations/errors/failed_transactions', params: t.type({ query: t.intersection([ @@ -174,11 +180,12 @@ export const correlationsForFailedTransactionsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { environment, kuery, @@ -186,7 +193,7 @@ export const correlationsForFailedTransactionsRoute = createRoute({ transactionType, transactionName, fieldNames, - } = context.params.query; + } = params.query; return getCorrelationsForFailedTransactions({ environment, @@ -199,3 +206,9 @@ export const correlationsForFailedTransactionsRoute = createRoute({ }); }, }); + +export const correlationsRouteRepository = createApmServerRouteRepository() + .add(correlationsLatencyDistributionRoute) + .add(correlationsForSlowTransactionsRoute) + .add(correlationsErrorDistributionRoute) + .add(correlationsForFailedTransactionsRoute); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts deleted file mode 100644 index 9958b8dec0124..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ /dev/null @@ -1,368 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { createApi } from './index'; -import { CoreSetup, Logger } from 'src/core/server'; -import { RouteParamsRT } from '../typings'; -import { BehaviorSubject } from 'rxjs'; -import { APMConfig } from '../..'; -import { jsonRt } from '../../../common/runtime_types/json_rt'; - -const getCoreMock = () => { - const get = jest.fn(); - const post = jest.fn(); - const put = jest.fn(); - const createRouter = jest.fn().mockReturnValue({ - get, - post, - put, - }); - - const mock = {} as CoreSetup; - - return { - mock: { - ...mock, - http: { - ...mock.http, - createRouter, - }, - }, - get, - post, - put, - createRouter, - context: { - measure: () => undefined, - config$: new BehaviorSubject({} as APMConfig), - logger: ({ - error: jest.fn(), - } as unknown) as Logger, - plugins: {}, - }, - }; -}; - -const initApi = (params?: RouteParamsRT) => { - const { mock, context, createRouter, get, post } = getCoreMock(); - const handlerMock = jest.fn(); - createApi() - .add(() => ({ - endpoint: 'GET /foo', - params, - options: { tags: ['access:apm'] }, - handler: handlerMock, - })) - .init(mock, context); - - const routeHandler = get.mock.calls[0][1]; - - const responseMock = { - ok: jest.fn(), - custom: jest.fn(), - }; - - const simulateRequest = (requestMock: any) => { - return routeHandler( - {}, - { - // stub default values - params: {}, - query: {}, - body: null, - ...requestMock, - }, - responseMock - ); - }; - - return { - simulateRequest, - handlerMock, - createRouter, - get, - post, - responseMock, - }; -}; - -describe('createApi', () => { - it('registers a route with the server', () => { - const { mock, context, createRouter, post, get, put } = getCoreMock(); - - createApi() - .add(() => ({ - endpoint: 'GET /foo', - options: { tags: ['access:apm'] }, - handler: async () => ({}), - })) - .add(() => ({ - endpoint: 'POST /bar', - params: t.type({ - body: t.string, - }), - options: { tags: ['access:apm'] }, - handler: async () => ({}), - })) - .add(() => ({ - endpoint: 'PUT /baz', - options: { - tags: ['access:apm', 'access:apm_write'], - }, - handler: async () => ({}), - })) - .add({ - endpoint: 'GET /qux', - options: { - tags: ['access:apm', 'access:apm_write'], - }, - handler: async () => ({}), - }) - .init(mock, context); - - expect(createRouter).toHaveBeenCalledTimes(1); - - expect(get).toHaveBeenCalledTimes(2); - expect(post).toHaveBeenCalledTimes(1); - expect(put).toHaveBeenCalledTimes(1); - - expect(get.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm'], - }, - path: '/foo', - validate: expect.anything(), - }); - - expect(get.mock.calls[1][0]).toEqual({ - options: { - tags: ['access:apm', 'access:apm_write'], - }, - path: '/qux', - validate: expect.anything(), - }); - - expect(post.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm'], - }, - path: '/bar', - validate: expect.anything(), - }); - - expect(put.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm', 'access:apm_write'], - }, - path: '/baz', - validate: expect.anything(), - }); - }); - - describe('when validating', () => { - describe('_inspect', () => { - it('allows _inspect=true', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi(); - await simulateRequest({ query: { _inspect: 'true' } }); - - const params = handlerMock.mock.calls[0][0].context.params; - expect(params).toEqual({ query: { _inspect: true } }); - expect(handlerMock).toHaveBeenCalledTimes(1); - - // responds with ok - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(responseMock.ok).toHaveBeenCalledWith({ - body: { _inspect: [] }, - }); - }); - - it('rejects _inspect=1', async () => { - const { simulateRequest, responseMock } = initApi(); - await simulateRequest({ query: { _inspect: 1 } }); - - // responds with error handler - expect(responseMock.ok).not.toHaveBeenCalled(); - expect(responseMock.custom).toHaveBeenCalledWith({ - body: { - attributes: { _inspect: [] }, - message: - 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', - }, - statusCode: 400, - }); - }); - - it('allows omitting _inspect', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi(); - await simulateRequest({ query: {} }); - - const params = handlerMock.mock.calls[0][0].context.params; - expect(params).toEqual({ query: { _inspect: false } }); - expect(handlerMock).toHaveBeenCalledTimes(1); - - // responds with ok - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(responseMock.ok).toHaveBeenCalledWith({ body: {} }); - }); - }); - - it('throws if unknown parameters are provided', async () => { - const { simulateRequest, responseMock } = initApi(); - - await simulateRequest({ - query: { _inspect: true, extra: '' }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - - await simulateRequest({ - body: { foo: 'bar' }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(2); - - await simulateRequest({ - params: { - foo: 'bar', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(3); - }); - - it('validates path parameters', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi( - t.type({ - path: t.type({ - foo: t.string, - }), - }) - ); - - await simulateRequest({ - params: { - foo: 'bar', - }, - }); - - expect(handlerMock).toHaveBeenCalledTimes(1); - - expect(responseMock.ok).toHaveBeenCalledTimes(1); - expect(responseMock.custom).not.toHaveBeenCalled(); - - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - path: { - foo: 'bar', - }, - query: { - _inspect: false, - }, - }); - - await simulateRequest({ - params: { - bar: 'foo', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - - await simulateRequest({ - params: { - foo: 9, - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(2); - - await simulateRequest({ - params: { - foo: 'bar', - extra: '', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(3); - }); - - it('validates body parameters', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi( - t.type({ - body: t.string, - }) - ); - - await simulateRequest({ - body: '', - }); - - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(responseMock.ok).toHaveBeenCalledTimes(1); - - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - body: '', - query: { - _inspect: false, - }, - }); - - await simulateRequest({ - body: null, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - }); - - it('validates query parameters', async () => { - const { simulateRequest, handlerMock, responseMock } = initApi( - t.type({ - query: t.type({ - bar: t.string, - filterNames: jsonRt.pipe(t.array(t.string)), - }), - }) - ); - - await simulateRequest({ - query: { - bar: '', - _inspect: 'true', - filterNames: JSON.stringify(['hostName', 'agentName']), - }, - }); - - expect(responseMock.custom).not.toHaveBeenCalled(); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(responseMock.ok).toHaveBeenCalledTimes(1); - - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - query: { - bar: '', - _inspect: true, - filterNames: ['hostName', 'agentName'], - }, - }); - - await simulateRequest({ - query: { - bar: '', - foo: '', - }, - }); - - expect(responseMock.custom).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts deleted file mode 100644 index 13e70a2043cf0..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ /dev/null @@ -1,184 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { merge as mergeLodash, pickBy, isEmpty, isPlainObject } from 'lodash'; -import Boom from '@hapi/boom'; -import { schema } from '@kbn/config-schema'; -import * as t from 'io-ts'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { KibanaRequest, RouteRegistrar } from 'src/core/server'; -import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; -import agent from 'elastic-apm-node'; -import { parseMethod } from '../../../common/apm_api/parse_endpoint'; -import { merge } from '../../../common/runtime_types/merge'; -import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt'; -import { APMConfig } from '../..'; -import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings'; -import { jsonRt } from '../../../common/runtime_types/json_rt'; -import type { ApmPluginRequestHandlerContext } from '../typings'; - -const inspectRt = t.exact( - t.partial({ - query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), - }) -); - -type RouteOrRouteFactoryFn = Parameters['add']>[0]; - -const isNotEmpty = (val: any) => - val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val)); - -export const inspectableEsQueriesMap = new WeakMap< - KibanaRequest, - InspectResponse ->(); - -export function createApi() { - const routes: RouteOrRouteFactoryFn[] = []; - const api: ServerAPI<{}> = { - _S: {}, - add(route) { - routes.push((route as unknown) as RouteOrRouteFactoryFn); - return this as any; - }, - init(core, { config$, logger, plugins }) { - const router = core.http.createRouter(); - - let config = {} as APMConfig; - - config$.subscribe((val) => { - config = val; - }); - - routes.forEach((routeOrFactoryFn) => { - const route = - typeof routeOrFactoryFn === 'function' - ? routeOrFactoryFn(core) - : routeOrFactoryFn; - - const { params, endpoint, options, handler } = route; - - const [method, path] = endpoint.split(' '); - const typedRouterMethod = parseMethod(method); - - // For all runtime types with props, we create an exact - // version that will strip all keys that are unvalidated. - const anyObject = schema.object({}, { unknowns: 'allow' }); - - (router[typedRouterMethod] as RouteRegistrar< - typeof typedRouterMethod, - ApmPluginRequestHandlerContext - >)( - { - path, - options, - validate: { - // `body` can be null, but `validate` expects non-nullable types - // if any validation is defined. Not having validation currently - // means we don't get the payload. See - // https://github.com/elastic/kibana/issues/50179 - body: schema.nullable(anyObject), - params: anyObject, - query: anyObject, - }, - }, - async (context, request, response) => { - if (agent.isStarted()) { - agent.addLabels({ - plugin: 'apm', - }); - } - - // init debug queries - inspectableEsQueriesMap.set(request, []); - - try { - const validParams = validateParams(request, params); - const data = await handler({ - request, - context: { - ...context, - plugins, - params: validParams, - config, - logger, - }, - }); - - const body = { ...data }; - if (validParams.query._inspect) { - body._inspect = inspectableEsQueriesMap.get(request); - } - - // cleanup - inspectableEsQueriesMap.delete(request); - - return response.ok({ body }); - } catch (error) { - const opts = { - statusCode: 500, - body: { - message: error.message, - attributes: { - _inspect: inspectableEsQueriesMap.get(request), - }, - }, - }; - - if (Boom.isBoom(error)) { - opts.statusCode = error.output.statusCode; - } - - if (error instanceof RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; - } - - return response.custom(opts); - } - } - ); - }); - }, - }; - - return api; -} - -function validateParams( - request: KibanaRequest, - params: RouteParamsRT | undefined -) { - const paramsRt = params ? merge([params, inspectRt]) : inspectRt; - const paramMap = pickBy( - { - path: request.params, - body: request.body, - query: { - _inspect: 'false', - // @ts-ignore - ...request.query, - }, - }, - isNotEmpty - ); - - const result = strictKeysRt(paramsRt).decode(paramMap); - - if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); - } - - // Only return values for parameters that have runtime types, - // but always include query as _inspect is always set even if - // it's not defined in the route. - return mergeLodash( - { query: { _inspect: false } }, - pickBy(result.right, isNotEmpty) - ); -} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts deleted file mode 100644 index 5b74aa4347f14..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ /dev/null @@ -1,230 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - staticIndexPatternRoute, - dynamicIndexPatternRoute, - apmIndexPatternTitleRoute, -} from './index_pattern'; -import { createApi } from './create_api'; -import { environmentsRoute } from './environments'; -import { - errorDistributionRoute, - errorGroupsRoute, - errorsRoute, -} from './errors'; -import { - serviceAgentNameRoute, - serviceTransactionTypesRoute, - servicesRoute, - serviceNodeMetadataRoute, - serviceAnnotationsRoute, - serviceAnnotationsCreateRoute, - serviceErrorGroupsPrimaryStatisticsRoute, - serviceErrorGroupsComparisonStatisticsRoute, - serviceThroughputRoute, - serviceDependenciesRoute, - serviceMetadataDetailsRoute, - serviceMetadataIconsRoute, - serviceInstancesPrimaryStatisticsRoute, - serviceInstancesComparisonStatisticsRoute, - serviceProfilingStatisticsRoute, - serviceProfilingTimelineRoute, -} from './services'; -import { - agentConfigurationRoute, - getSingleAgentConfigurationRoute, - agentConfigurationSearchRoute, - deleteAgentConfigurationRoute, - listAgentConfigurationEnvironmentsRoute, - listAgentConfigurationServicesRoute, - createOrUpdateAgentConfigurationRoute, - agentConfigurationAgentNameRoute, -} from './settings/agent_configuration'; -import { - apmIndexSettingsRoute, - apmIndicesRoute, - saveApmIndicesRoute, -} from './settings/apm_indices'; -import { metricsChartsRoute } from './metrics'; -import { serviceNodesRoute } from './service_nodes'; -import { - tracesRoute, - tracesByIdRoute, - rootTransactionByTraceIdRoute, -} from './traces'; -import { - correlationsLatencyDistributionRoute, - correlationsForSlowTransactionsRoute, - correlationsErrorDistributionRoute, - correlationsForFailedTransactionsRoute, -} from './correlations'; -import { - transactionChartsBreakdownRoute, - transactionChartsDistributionRoute, - transactionChartsErrorRateRoute, - transactionGroupsRoute, - transactionGroupsPrimaryStatisticsRoute, - transactionLatencyChartsRoute, - transactionThroughputChartsRoute, - transactionGroupsComparisonStatisticsRoute, -} from './transactions'; -import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; -import { - createCustomLinkRoute, - updateCustomLinkRoute, - deleteCustomLinkRoute, - listCustomLinksRoute, - customLinkTransactionRoute, -} from './settings/custom_link'; -import { - observabilityOverviewHasDataRoute, - observabilityOverviewRoute, -} from './observability_overview'; -import { - anomalyDetectionJobsRoute, - createAnomalyDetectionJobsRoute, - anomalyDetectionEnvironmentsRoute, -} from './settings/anomaly_detection'; -import { - rumHasDataRoute, - rumClientMetricsRoute, - rumJSErrors, - rumLongTaskMetrics, - rumOverviewLocalFiltersRoute, - rumPageLoadDistBreakdownRoute, - rumPageLoadDistributionRoute, - rumPageViewsTrendRoute, - rumServicesRoute, - rumUrlSearch, - rumVisitorsBreakdownRoute, - rumWebCoreVitals, -} from './rum_client'; -import { - transactionErrorRateChartPreview, - transactionErrorCountChartPreview, - transactionDurationChartPreview, -} from './alerts/chart_preview'; - -const createApmApi = () => { - const api = createApi() - // index pattern - .add(staticIndexPatternRoute) - .add(dynamicIndexPatternRoute) - .add(apmIndexPatternTitleRoute) - - // Environments - .add(environmentsRoute) - - // Errors - .add(errorDistributionRoute) - .add(errorGroupsRoute) - .add(errorsRoute) - - // Services - .add(serviceAgentNameRoute) - .add(serviceTransactionTypesRoute) - .add(servicesRoute) - .add(serviceNodeMetadataRoute) - .add(serviceAnnotationsRoute) - .add(serviceAnnotationsCreateRoute) - .add(serviceErrorGroupsPrimaryStatisticsRoute) - .add(serviceThroughputRoute) - .add(serviceDependenciesRoute) - .add(serviceMetadataDetailsRoute) - .add(serviceMetadataIconsRoute) - .add(serviceInstancesPrimaryStatisticsRoute) - .add(serviceInstancesComparisonStatisticsRoute) - .add(serviceErrorGroupsComparisonStatisticsRoute) - .add(serviceProfilingTimelineRoute) - .add(serviceProfilingStatisticsRoute) - - // Agent configuration - .add(getSingleAgentConfigurationRoute) - .add(agentConfigurationAgentNameRoute) - .add(agentConfigurationRoute) - .add(agentConfigurationSearchRoute) - .add(deleteAgentConfigurationRoute) - .add(listAgentConfigurationEnvironmentsRoute) - .add(listAgentConfigurationServicesRoute) - .add(createOrUpdateAgentConfigurationRoute) - - // Correlations - .add(correlationsLatencyDistributionRoute) - .add(correlationsForSlowTransactionsRoute) - .add(correlationsErrorDistributionRoute) - .add(correlationsForFailedTransactionsRoute) - - // APM indices - .add(apmIndexSettingsRoute) - .add(apmIndicesRoute) - .add(saveApmIndicesRoute) - - // Metrics - .add(metricsChartsRoute) - .add(serviceNodesRoute) - - // Traces - .add(tracesRoute) - .add(tracesByIdRoute) - .add(rootTransactionByTraceIdRoute) - - // Transactions - .add(transactionChartsBreakdownRoute) - .add(transactionChartsDistributionRoute) - .add(transactionChartsErrorRateRoute) - .add(transactionGroupsRoute) - .add(transactionGroupsPrimaryStatisticsRoute) - .add(transactionLatencyChartsRoute) - .add(transactionThroughputChartsRoute) - .add(transactionGroupsComparisonStatisticsRoute) - - // Service map - .add(serviceMapRoute) - .add(serviceMapServiceNodeRoute) - - // Custom links - .add(createCustomLinkRoute) - .add(updateCustomLinkRoute) - .add(deleteCustomLinkRoute) - .add(listCustomLinksRoute) - .add(customLinkTransactionRoute) - - // Observability dashboard - .add(observabilityOverviewHasDataRoute) - .add(observabilityOverviewRoute) - - // Anomaly detection - .add(anomalyDetectionJobsRoute) - .add(createAnomalyDetectionJobsRoute) - .add(anomalyDetectionEnvironmentsRoute) - - // User Experience app api routes - .add(rumOverviewLocalFiltersRoute) - .add(rumPageViewsTrendRoute) - .add(rumPageLoadDistributionRoute) - .add(rumPageLoadDistBreakdownRoute) - .add(rumClientMetricsRoute) - .add(rumServicesRoute) - .add(rumVisitorsBreakdownRoute) - .add(rumWebCoreVitals) - .add(rumJSErrors) - .add(rumUrlSearch) - .add(rumLongTaskMetrics) - .add(rumHasDataRoute) - - // Alerting - .add(transactionErrorCountChartPreview) - .add(transactionDurationChartPreview) - .add(transactionErrorRateChartPreview); - - return api; -}; - -export type APMAPI = ReturnType; - -export { createApmApi }; diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route.ts new file mode 100644 index 0000000000000..86330a87a8c55 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/create_apm_server_route.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createServerRouteFactory } from '@kbn/server-route-repository'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings'; + +export const createApmServerRoute = createServerRouteFactory< + APMRouteHandlerResources, + APMRouteCreateOptions +>(); diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts new file mode 100644 index 0000000000000..b7cbe890c57db --- /dev/null +++ b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createServerRouteRepository } from '@kbn/server-route-repository'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings'; + +export function createApmServerRouteRepository() { + return createServerRouteRepository< + APMRouteHandlerResources, + APMRouteCreateOptions + >(); +} diff --git a/x-pack/plugins/apm/server/routes/create_route.ts b/x-pack/plugins/apm/server/routes/create_route.ts deleted file mode 100644 index d74aac0992eb4..0000000000000 --- a/x-pack/plugins/apm/server/routes/create_route.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup } from 'src/core/server'; -import { HandlerReturn, Route, RouteParamsRT } from './typings'; - -export function createRoute< - TEndpoint extends string, - TReturn extends HandlerReturn, - TRouteParamsRT extends RouteParamsRT | undefined = undefined ->( - route: Route -): Route; - -export function createRoute< - TEndpoint extends string, - TReturn extends HandlerReturn, - TRouteParamsRT extends RouteParamsRT | undefined = undefined ->( - route: (core: CoreSetup) => Route -): (core: CoreSetup) => Route; - -export function createRoute(routeOrFactoryFn: Function | object) { - return routeOrFactoryFn; -} diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index 4aa7d7e6d412f..e06fbdf7fb6d4 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -9,10 +9,11 @@ import * as t from 'io-ts'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/environments/get_environments'; -import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const environmentsRoute = createRoute({ +const environmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/environments', params: t.type({ query: t.intersection([ @@ -23,9 +24,10 @@ export const environmentsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -39,3 +41,7 @@ export const environmentsRoute = createRoute({ return { environments }; }, }); + +export const environmentsRouteRepository = createApmServerRouteRepository().add( + environmentsRoute +); diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index f69d3fc9631d1..d6bb1d4bcbaae 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -6,14 +6,15 @@ */ import * as t from 'io-ts'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; import { getErrorDistribution } from '../lib/errors/distribution/get_distribution'; import { getErrorGroupSample } from '../lib/errors/get_error_group_sample'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const errorsRoute = createRoute({ +const errorsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors', params: t.type({ path: t.type({ @@ -30,9 +31,9 @@ export const errorsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); const { serviceName } = params.path; const { environment, kuery, sortField, sortDirection } = params.query; @@ -49,7 +50,7 @@ export const errorsRoute = createRoute({ }, }); -export const errorGroupsRoute = createRoute({ +const errorGroupsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}', params: t.type({ path: t.type({ @@ -59,10 +60,11 @@ export const errorGroupsRoute = createRoute({ query: t.intersection([environmentRt, kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName, groupId } = context.params.path; - const { environment, kuery } = context.params.query; + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); + const { serviceName, groupId } = params.path; + const { environment, kuery } = params.query; return getErrorGroupSample({ environment, @@ -74,7 +76,7 @@ export const errorGroupsRoute = createRoute({ }, }); -export const errorDistributionRoute = createRoute({ +const errorDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', params: t.type({ path: t.type({ @@ -90,9 +92,9 @@ export const errorDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { serviceName } = params.path; const { environment, kuery, groupId } = params.query; return getErrorDistribution({ @@ -104,3 +106,8 @@ export const errorDistributionRoute = createRoute({ }); }, }); + +export const errorsRouteRepository = createApmServerRouteRepository() + .add(errorsRoute) + .add(errorGroupsRoute) + .add(errorDistributionRoute); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts new file mode 100644 index 0000000000000..c151752b4b6e0 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + ServerRouteRepository, + ReturnOf, + EndpointOf, +} from '@kbn/server-route-repository'; +import { PickByValue } from 'utility-types'; +import { alertsChartPreviewRouteRepository } from './alerts/chart_preview'; +import { correlationsRouteRepository } from './correlations'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { environmentsRouteRepository } from './environments'; +import { errorsRouteRepository } from './errors'; +import { indexPatternRouteRepository } from './index_pattern'; +import { metricsRouteRepository } from './metrics'; +import { observabilityOverviewRouteRepository } from './observability_overview'; +import { rumRouteRepository } from './rum_client'; +import { serviceRouteRepository } from './services'; +import { serviceMapRouteRepository } from './service_map'; +import { serviceNodeRouteRepository } from './service_nodes'; +import { agentConfigurationRouteRepository } from './settings/agent_configuration'; +import { anomalyDetectionRouteRepository } from './settings/anomaly_detection'; +import { apmIndicesRouteRepository } from './settings/apm_indices'; +import { customLinkRouteRepository } from './settings/custom_link'; +import { traceRouteRepository } from './traces'; +import { transactionRouteRepository } from './transactions'; +import { APMRouteHandlerResources } from './typings'; + +const getTypedGlobalApmServerRouteRepository = () => { + const repository = createApmServerRouteRepository() + .merge(indexPatternRouteRepository) + .merge(environmentsRouteRepository) + .merge(errorsRouteRepository) + .merge(metricsRouteRepository) + .merge(observabilityOverviewRouteRepository) + .merge(rumRouteRepository) + .merge(serviceMapRouteRepository) + .merge(serviceNodeRouteRepository) + .merge(serviceRouteRepository) + .merge(traceRouteRepository) + .merge(transactionRouteRepository) + .merge(alertsChartPreviewRouteRepository) + .merge(correlationsRouteRepository) + .merge(agentConfigurationRouteRepository) + .merge(anomalyDetectionRouteRepository) + .merge(apmIndicesRouteRepository) + .merge(customLinkRouteRepository); + + return repository; +}; + +const getGlobalApmServerRouteRepository = () => { + return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository; +}; + +export type APMServerRouteRepository = ReturnType< + typeof getTypedGlobalApmServerRouteRepository +>; + +// Ensure no APIs return arrays (or, by proxy, the any type), +// to guarantee compatibility with _inspect. + +type CompositeEndpoint = EndpointOf; + +type EndpointReturnTypes = { + [Endpoint in CompositeEndpoint]: ReturnOf; +}; + +type ArrayLikeReturnTypes = PickByValue; + +type ViolatingEndpoints = keyof ArrayLikeReturnTypes; + +function assertType() {} + +// if any endpoint has an array-like return type, the assertion below will fail +assertType(); + +export { getGlobalApmServerRouteRepository }; diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index 3b800c23135ce..aa70cde4f96ae 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -6,49 +6,67 @@ */ import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern'; -import { createRoute } from './create_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern'; +import { createApmServerRoute } from './create_apm_server_route'; -export const staticIndexPatternRoute = createRoute((core) => ({ +const staticIndexPatternRoute = createApmServerRoute({ endpoint: 'POST /api/apm/index_pattern/static', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { + request, + core, + plugins: { spaces }, + config, + } = resources; + const [setup, savedObjectsClient] = await Promise.all([ - setupRequest(context, request), - getInternalSavedObjectsClient(core), + setupRequest(resources), + core + .start() + .then((coreStart) => coreStart.savedObjects.createInternalRepository()), ]); - const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request); + const spaceId = spaces?.setup.spacesService.getSpaceId(request); const didCreateIndexPattern = await createStaticIndexPattern( setup, - context, + config, savedObjectsClient, spaceId ); return { created: didCreateIndexPattern }; }, -})); +}); -export const dynamicIndexPatternRoute = createRoute({ +const dynamicIndexPatternRoute = createApmServerRoute({ endpoint: 'GET /api/apm/index_pattern/dynamic', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { - const dynamicIndexPattern = await getDynamicIndexPattern({ context }); + handler: async ({ context, config, logger }) => { + const dynamicIndexPattern = await getDynamicIndexPattern({ + context, + config, + logger, + }); return { dynamicIndexPattern }; }, }); -export const apmIndexPatternTitleRoute = createRoute({ +const indexPatternTitleRoute = createApmServerRoute({ endpoint: 'GET /api/apm/index_pattern/title', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { + handler: async ({ config }) => { return { - indexPatternTitle: getApmIndexPatternTitle(context), + indexPatternTitle: getApmIndexPatternTitle(config), }; }, }); + +export const indexPatternRouteRepository = createApmServerRouteRepository() + .add(staticIndexPatternRoute) + .add(dynamicIndexPatternRoute) + .add(indexPatternTitleRoute); diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index c7e82e13d07b8..9fa2346eb72fb 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -8,10 +8,11 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; -export const metricsChartsRoute = createRoute({ +const metricsChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/metrics/charts', params: t.type({ path: t.type({ @@ -30,9 +31,9 @@ export const metricsChartsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); const { serviceName } = params.path; const { agentName, environment, kuery, serviceNodeName } = params.query; return await getMetricsChartDataByAgent({ @@ -45,3 +46,7 @@ export const metricsChartsRoute = createRoute({ }); }, }); + +export const metricsRouteRepository = createApmServerRouteRepository().add( + metricsChartsRoute +); diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index b9c0a76b6fb90..d459570cf7337 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -8,49 +8,55 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; -import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates'; +import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; import { getHasData } from '../lib/observability_overview/has_data'; -import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { withApmSpan } from '../utils/with_apm_span'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './create_apm_server_route'; -export const observabilityOverviewHasDataRoute = createRoute({ +const observabilityOverviewHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_data', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const res = await getHasData({ setup }); return { hasData: res }; }, }); -export const observabilityOverviewRoute = createRoute({ +const observabilityOverviewRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview', params: t.type({ query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { bucketSize } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { bucketSize } = resources.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); return withApmSpan('observability_overview', async () => { - const [serviceCount, transactionCoordinates] = await Promise.all([ + const [serviceCount, transactionPerMinute] = await Promise.all([ getServiceCount({ setup, searchAggregatedTransactions, }), - getTransactionCoordinates({ + getTransactionsPerMinute({ setup, bucketSize, searchAggregatedTransactions, }), ]); - return { serviceCount, transactionCoordinates }; + return { serviceCount, transactionPerMinute }; }); }, }); + +export const observabilityOverviewRouteRepository = createApmServerRouteRepository() + .add(observabilityOverviewRoute) + .add(observabilityOverviewHasDataRoute); diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts new file mode 100644 index 0000000000000..82b73d46da5c1 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts @@ -0,0 +1,507 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { jsonRt } from '@kbn/io-ts-utils'; +import { createServerRouteRepository } from '@kbn/server-route-repository'; +import { ServerRoute } from '@kbn/server-route-repository/target/typings'; +import * as t from 'io-ts'; +import { CoreSetup, Logger } from 'src/core/server'; +import { APMConfig } from '../..'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings'; +import { registerRoutes } from './index'; + +type RegisterRouteDependencies = Parameters[0]; + +const getRegisterRouteDependencies = () => { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const createRouter = jest.fn().mockReturnValue({ + get, + post, + put, + }); + + const coreSetup = ({ + http: { + createRouter, + }, + } as unknown) as CoreSetup; + + const logger = ({ + error: jest.fn(), + } as unknown) as Logger; + + return { + mocks: { + get, + post, + put, + createRouter, + coreSetup, + logger, + }, + dependencies: ({ + core: { + setup: coreSetup, + }, + logger, + config: {} as APMConfig, + plugins: {}, + } as unknown) as RegisterRouteDependencies, + }; +}; + +const getRepository = () => + createServerRouteRepository< + APMRouteHandlerResources, + APMRouteCreateOptions + >(); + +const initApi = ( + routes: Array< + ServerRoute< + any, + t.Any, + APMRouteHandlerResources, + any, + APMRouteCreateOptions + > + > +) => { + const { mocks, dependencies } = getRegisterRouteDependencies(); + + let repository = getRepository(); + + routes.forEach((route) => { + repository = repository.add(route); + }); + + registerRoutes({ + ...dependencies, + repository, + }); + + const responseMock = { + ok: jest.fn(), + custom: jest.fn(), + }; + + const simulateRequest = (request: { + method: 'get' | 'post' | 'put'; + pathname: string; + params?: Record; + body?: unknown; + query?: Record; + }) => { + const [, registeredRouteHandler] = + mocks[request.method].mock.calls.find((call) => { + return call[0].path === request.pathname; + }) ?? []; + + const result = registeredRouteHandler( + {}, + { + params: {}, + query: {}, + body: null, + ...request, + }, + responseMock + ); + + return result; + }; + + return { + simulateRequest, + mocks: { + ...mocks, + response: responseMock, + }, + }; +}; + +describe('createApi', () => { + it('registers a route with the server', () => { + const { + mocks: { createRouter, get, post, put }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: ['access:apm'] }, + handler: async () => ({}), + }, + { + endpoint: 'POST /bar', + params: t.type({ + body: t.string, + }), + options: { tags: ['access:apm'] }, + handler: async () => ({}), + }, + { + endpoint: 'PUT /baz', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => ({}), + }, + { + endpoint: 'GET /qux', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => ({}), + }, + ]); + + expect(createRouter).toHaveBeenCalledTimes(1); + + expect(get).toHaveBeenCalledTimes(2); + expect(post).toHaveBeenCalledTimes(1); + expect(put).toHaveBeenCalledTimes(1); + + expect(get.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm'], + }, + path: '/foo', + validate: expect.anything(), + }); + + expect(get.mock.calls[1][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/qux', + validate: expect.anything(), + }); + + expect(post.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm'], + }, + path: '/bar', + validate: expect.anything(), + }); + + expect(put.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/baz', + validate: expect.anything(), + }); + }); + + describe('when validating', () => { + describe('_inspect', () => { + it('allows _inspect=true', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 'true' }, + }); + + // responds with ok + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + expect(params).toEqual({ query: { _inspect: true } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledWith({ + body: { _inspect: [] }, + }); + }); + + it('rejects _inspect=1', async () => { + const handlerMock = jest.fn(); + + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + handler: handlerMock, + }, + ]); + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 1 }, + }); + + // responds with error handler + expect(response.ok).not.toHaveBeenCalled(); + expect(response.custom).toHaveBeenCalledWith({ + body: { + attributes: { _inspect: [] }, + message: + 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', + }, + statusCode: 400, + }); + }); + + it('allows omitting _inspect', async () => { + const handlerMock = jest.fn(); + + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock }, + ]); + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: {}, + }); + + // responds with ok + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + expect(params).toEqual({ query: { _inspect: false } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + + expect(response.ok).toHaveBeenCalledWith({ body: {} }); + }); + }); + + it('throws if unknown parameters are provided', async () => { + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 'true', extra: '' }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: { foo: 'bar' }, + }); + + expect(response.custom).toHaveBeenCalledTimes(2); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(3); + }); + + it('validates path parameters', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: [] }, + params: t.type({ + path: t.type({ + foo: t.string, + }), + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + }, + }); + + expect(handlerMock).toHaveBeenCalledTimes(1); + + expect(response.ok).toHaveBeenCalledTimes(1); + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + path: { + foo: 'bar', + }, + query: { + _inspect: false, + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + bar: 'foo', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 9, + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(2); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + extra: '', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(3); + }); + + it('validates body parameters', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + params: t.type({ + body: t.string, + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: '', + }); + + expect(response.custom).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledTimes(1); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + body: '', + query: { + _inspect: false, + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: null, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + }); + + it('validates query parameters', async () => { + const handlerMock = jest.fn(); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + params: t.type({ + query: t.type({ + bar: t.string, + filterNames: jsonRt.pipe(t.array(t.string)), + }), + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { + bar: '', + _inspect: 'true', + filterNames: JSON.stringify(['hostName', 'agentName']), + }, + }); + + expect(response.custom).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledTimes(1); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + query: { + bar: '', + _inspect: true, + filterNames: ['hostName', 'agentName'], + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { + bar: '', + foo: '', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts new file mode 100644 index 0000000000000..3a88a496b923f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import * as t from 'io-ts'; +import { KibanaRequest, RouteRegistrar } from 'src/core/server'; +import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; +import agent from 'elastic-apm-node'; +import { ServerRouteRepository } from '@kbn/server-route-repository'; +import { merge } from 'lodash'; +import { + decodeRequestParams, + parseEndpoint, + routeValidationObject, +} from '@kbn/server-route-repository'; +import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; +import { pickKeys } from '../../../common/utils/pick_keys'; +import { APMRouteHandlerResources, InspectResponse } from '../typings'; +import type { ApmPluginRequestHandlerContext } from '../typings'; + +const inspectRt = t.exact( + t.partial({ + query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), + }) +); + +export const inspectableEsQueriesMap = new WeakMap< + KibanaRequest, + InspectResponse +>(); + +export function registerRoutes({ + core, + repository, + plugins, + logger, + config, +}: { + core: APMRouteHandlerResources['core']; + plugins: APMRouteHandlerResources['plugins']; + logger: APMRouteHandlerResources['logger']; + repository: ServerRouteRepository; + config: APMRouteHandlerResources['config']; +}) { + const routes = repository.getRoutes(); + + const router = core.setup.http.createRouter(); + + routes.forEach((route) => { + const { params, endpoint, options, handler } = route; + + const { method, pathname } = parseEndpoint(endpoint); + + (router[method] as RouteRegistrar< + typeof method, + ApmPluginRequestHandlerContext + >)( + { + path: pathname, + options, + validate: routeValidationObject, + }, + async (context, request, response) => { + if (agent.isStarted()) { + agent.addLabels({ + plugin: 'apm', + }); + } + + // init debug queries + inspectableEsQueriesMap.set(request, []); + + try { + const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt; + + const validatedParams = decodeRequestParams( + pickKeys(request, 'params', 'body', 'query'), + runtimeType + ); + + const data: Record | undefined | null = (await handler({ + request, + context, + config, + logger, + core, + plugins, + params: merge( + { + query: { + _inspect: false, + }, + }, + validatedParams + ), + })) as any; + + if (Array.isArray(data)) { + throw new Error('Return type cannot be an array'); + } + + const body = validatedParams.query?._inspect + ? { + ...data, + _inspect: inspectableEsQueriesMap.get(request), + } + : { ...data }; + + // cleanup + inspectableEsQueriesMap.delete(request); + + return response.ok({ body }); + } catch (error) { + logger.error(error); + const opts = { + statusCode: 500, + body: { + message: error.message, + attributes: { + _inspect: inspectableEsQueriesMap.get(request), + }, + }, + }; + + if (Boom.isBoom(error)) { + opts.statusCode = error.output.statusCode; + } + + if (error instanceof RequestAbortedError) { + opts.statusCode = 499; + opts.body.message = 'Client closed request'; + } + + return response.custom(opts); + } + } + ); + }); +} diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 3156acb469a72..d7f91adc0d683 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { jsonRt } from '../../common/runtime_types/json_rt'; +import { jsonRt } from '@kbn/io-ts-utils'; import { LocalUIFilterName } from '../../common/ui_filter'; import { Setup, @@ -28,9 +28,10 @@ import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config'; import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; import { Projection } from '../projections/typings'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { rangeRt } from './default_api_types'; -import { APMRequestHandlerContext } from './typings'; +import { APMRouteHandlerResources } from './typings'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -45,18 +46,18 @@ const uxQueryRt = t.intersection([ t.partial({ urlQuery: t.string, percentile: t.string }), ]); -export const rumClientMetricsRoute = createRoute({ +const rumClientMetricsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum/client-metrics', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getClientMetrics({ setup, @@ -66,18 +67,18 @@ export const rumClientMetricsRoute = createRoute({ }, }); -export const rumPageLoadDistributionRoute = createRoute({ +const rumPageLoadDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/page-load-distribution', params: t.type({ query: t.intersection([uxQueryRt, percentileRangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { minPercentile, maxPercentile, urlQuery }, - } = context.params; + } = resources.params; const pageLoadDistribution = await getPageLoadDistribution({ setup, @@ -90,7 +91,7 @@ export const rumPageLoadDistributionRoute = createRoute({ }, }); -export const rumPageLoadDistBreakdownRoute = createRoute({ +const rumPageLoadDistBreakdownRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown', params: t.type({ query: t.intersection([ @@ -100,12 +101,12 @@ export const rumPageLoadDistBreakdownRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { minPercentile, maxPercentile, breakdown, urlQuery }, - } = context.params; + } = resources.params; const pageLoadDistBreakdown = await getPageLoadDistBreakdown({ setup, @@ -119,18 +120,18 @@ export const rumPageLoadDistBreakdownRoute = createRoute({ }, }); -export const rumPageViewsTrendRoute = createRoute({ +const rumPageViewsTrendRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/page-view-trends', params: t.type({ query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { breakdowns, urlQuery }, - } = context.params; + } = resources.params; return getPageViewTrends({ setup, @@ -140,32 +141,32 @@ export const rumPageViewsTrendRoute = createRoute({ }, }); -export const rumServicesRoute = createRoute({ +const rumServicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/services', params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const rumServices = await getRumServices({ setup }); return { rumServices }; }, }); -export const rumVisitorsBreakdownRoute = createRoute({ +const rumVisitorsBreakdownRoute = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/visitor-breakdown', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery }, - } = context.params; + } = resources.params; return getVisitorBreakdown({ setup, @@ -174,18 +175,18 @@ export const rumVisitorsBreakdownRoute = createRoute({ }, }); -export const rumWebCoreVitals = createRoute({ +const rumWebCoreVitals = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/web-core-vitals', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getWebCoreVitals({ setup, @@ -195,18 +196,18 @@ export const rumWebCoreVitals = createRoute({ }, }); -export const rumLongTaskMetrics = createRoute({ +const rumLongTaskMetrics = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/long-task-metrics', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getLongTaskMetrics({ setup, @@ -216,24 +217,24 @@ export const rumLongTaskMetrics = createRoute({ }, }); -export const rumUrlSearch = createRoute({ +const rumUrlSearch = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/url-search', params: t.type({ query: uxQueryRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { urlQuery, percentile }, - } = context.params; + } = resources.params; return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) }); }, }); -export const rumJSErrors = createRoute({ +const rumJSErrors = createApmServerRoute({ endpoint: 'GET /api/apm/rum-client/js-errors', params: t.type({ query: t.intersection([ @@ -244,12 +245,12 @@ export const rumJSErrors = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { query: { pageSize, pageIndex, urlQuery }, - } = context.params; + } = resources.params; return getJSErrors({ setup, @@ -260,14 +261,14 @@ export const rumJSErrors = createRoute({ }, }); -export const rumHasDataRoute = createRoute({ +const rumHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); return await hasRumData({ setup }); }, }); @@ -309,21 +310,22 @@ function createLocalFiltersRoute< >; queryRt: TQueryRT; }) { - return createRoute({ + return createApmServerRoute({ endpoint, params: t.type({ query: t.intersection([localUiBaseQueryRt, queryRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const { uiFilters } = setup; - const { query } = context.params; + + const { query } = resources.params; const { filterNames } = query; const projection = await getProjection({ query, - context, + resources, setup, }); @@ -339,7 +341,7 @@ function createLocalFiltersRoute< }); } -export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ +const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ endpoint: 'GET /api/apm/rum/local_filters', getProjection: async ({ setup }) => { return getRumPageLoadTransactionsProjection({ @@ -357,9 +359,23 @@ type GetProjection< > = ({ query, setup, - context, + resources, }: { query: t.TypeOf; setup: Setup & SetupTimeRange; - context: APMRequestHandlerContext; + resources: APMRouteHandlerResources; }) => Promise | TProjection; + +export const rumRouteRepository = createApmServerRouteRepository() + .add(rumClientMetricsRoute) + .add(rumPageLoadDistributionRoute) + .add(rumPageLoadDistBreakdownRoute) + .add(rumPageViewsTrendRoute) + .add(rumServicesRoute) + .add(rumVisitorsBreakdownRoute) + .add(rumWebCoreVitals) + .add(rumLongTaskMetrics) + .add(rumUrlSearch) + .add(rumJSErrors) + .add(rumHasDataRoute) + .add(rumOverviewLocalFiltersRoute); diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 33943d6e05d01..267479de4c102 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -11,13 +11,14 @@ import { invalidLicenseMessage } from '../../common/service_map'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; import { environmentRt, rangeRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { isActivePlatinumLicense } from '../../common/license_check'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const serviceMapRoute = createRoute({ +const serviceMapRoute = createApmServerRoute({ endpoint: 'GET /api/apm/service-map', params: t.type({ query: t.intersection([ @@ -29,8 +30,9 @@ export const serviceMapRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - if (!context.config['xpack.apm.serviceMapEnabled']) { + handler: async (resources) => { + const { config, context, params, logger } = resources; + if (!config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } if (!isActivePlatinumLicense(context.licensing.license)) { @@ -42,11 +44,10 @@ export const serviceMapRoute = createRoute({ featureName: 'serviceMaps', }); - const logger = context.logger; - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { query: { serviceName, environment }, - } = context.params; + } = params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -61,7 +62,7 @@ export const serviceMapRoute = createRoute({ }, }); -export const serviceMapServiceNodeRoute = createRoute({ +const serviceMapServiceNodeRoute = createApmServerRoute({ endpoint: 'GET /api/apm/service-map/service/{serviceName}', params: t.type({ path: t.type({ @@ -70,19 +71,21 @@ export const serviceMapServiceNodeRoute = createRoute({ query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - if (!context.config['xpack.apm.serviceMapEnabled']) { + handler: async (resources) => { + const { config, context, params } = resources; + + if (!config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const { path: { serviceName }, query: { environment }, - } = context.params; + } = params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -96,3 +99,7 @@ export const serviceMapServiceNodeRoute = createRoute({ }); }, }); + +export const serviceMapRouteRepository = createApmServerRouteRepository() + .add(serviceMapRoute) + .add(serviceMapServiceNodeRoute); diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index e9060688c63a6..a2eb12662cbca 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -6,12 +6,13 @@ */ import * as t from 'io-ts'; -import { createRoute } from './create_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './create_apm_server_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNodes } from '../lib/service_nodes'; import { rangeRt, kueryRt } from './default_api_types'; -export const serviceNodesRoute = createRoute({ +const serviceNodesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes', params: t.type({ path: t.type({ @@ -20,9 +21,9 @@ export const serviceNodesRoute = createRoute({ query: t.intersection([kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { serviceName } = params.path; const { kuery } = params.query; @@ -30,3 +31,7 @@ export const serviceNodesRoute = createRoute({ return { serviceNodes }; }, }); + +export const serviceNodeRouteRepository = createApmServerRouteRepository().add( + serviceNodesRoute +); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index b4d25ca8b2a06..800a5bdcc5d5f 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -6,15 +6,12 @@ */ import Boom from '@hapi/boom'; +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { uniq } from 'lodash'; -import { - LatencyAggregationType, - latencyAggregationTypeRt, -} from '../../common/latency_aggregation_types'; +import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types'; import { ProfilingValueType } from '../../common/profiling'; import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt'; -import { jsonRt } from '../../common/runtime_types/json_rt'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; @@ -35,7 +32,8 @@ import { getServiceProfilingStatistics } from '../lib/services/profiling/get_ser import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline'; import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { withApmSpan } from '../utils/with_apm_span'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { comparisonRangeRt, environmentRt, @@ -43,15 +41,16 @@ import { rangeRt, } from './default_api_types'; -export const servicesRoute = createRoute({ +const servicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services', params: t.type({ query: t.intersection([environmentRt, kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { environment, kuery } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + const { environment, kuery } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -61,21 +60,22 @@ export const servicesRoute = createRoute({ kuery, setup, searchAggregatedTransactions, - logger: context.logger, + logger, }); }, }); -export const serviceMetadataDetailsRoute = createRoute({ +const serviceMetadataDetailsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/metadata/details', params: t.type({ path: t.type({ serviceName: t.string }), query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -89,16 +89,17 @@ export const serviceMetadataDetailsRoute = createRoute({ }, }); -export const serviceMetadataIconsRoute = createRoute({ +const serviceMetadataIconsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons', params: t.type({ path: t.type({ serviceName: t.string }), query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -112,7 +113,7 @@ export const serviceMetadataIconsRoute = createRoute({ }, }); -export const serviceAgentNameRoute = createRoute({ +const serviceAgentNameRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/agent_name', params: t.type({ path: t.type({ @@ -121,9 +122,10 @@ export const serviceAgentNameRoute = createRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -136,7 +138,7 @@ export const serviceAgentNameRoute = createRoute({ }, }); -export const serviceTransactionTypesRoute = createRoute({ +const serviceTransactionTypesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transaction_types', params: t.type({ path: t.type({ @@ -145,9 +147,11 @@ export const serviceTransactionTypesRoute = createRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + return getServiceTransactionTypes({ serviceName, setup, @@ -158,7 +162,7 @@ export const serviceTransactionTypesRoute = createRoute({ }, }); -export const serviceNodeMetadataRoute = createRoute({ +const serviceNodeMetadataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', params: t.type({ @@ -169,10 +173,11 @@ export const serviceNodeMetadataRoute = createRoute({ query: t.intersection([kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName, serviceNodeName } = context.params.path; - const { kuery } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName, serviceNodeName } = params.path; + const { kuery } = params.query; return getServiceNodeMetadata({ kuery, @@ -183,7 +188,7 @@ export const serviceNodeMetadataRoute = createRoute({ }, }); -export const serviceAnnotationsRoute = createRoute({ +const serviceAnnotationsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', params: t.type({ path: t.type({ @@ -192,12 +197,13 @@ export const serviceAnnotationsRoute = createRoute({ query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { environment } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, plugins, context, request, logger } = resources; + const { serviceName } = params.path; + const { environment } = params.query; - const { observability } = context.plugins; + const { observability } = plugins; const [ annotationsClient, @@ -205,7 +211,7 @@ export const serviceAnnotationsRoute = createRoute({ ] = await Promise.all([ observability ? withApmSpan('get_scoped_annotations_client', () => - observability.getScopedAnnotationsClient(context, request) + observability.setup.getScopedAnnotationsClient(context, request) ) : undefined, getSearchAggregatedTransactions(setup), @@ -218,12 +224,12 @@ export const serviceAnnotationsRoute = createRoute({ serviceName, annotationsClient, client: context.core.elasticsearch.client.asCurrentUser, - logger: context.logger, + logger, }); }, }); -export const serviceAnnotationsCreateRoute = createRoute({ +const serviceAnnotationsCreateRoute = createApmServerRoute({ endpoint: 'POST /api/apm/services/{serviceName}/annotation', options: { tags: ['access:apm', 'access:apm_write'], @@ -250,12 +256,17 @@ export const serviceAnnotationsCreateRoute = createRoute({ }), ]), }), - handler: async ({ request, context }) => { - const { observability } = context.plugins; + handler: async (resources) => { + const { + request, + context, + plugins: { observability }, + params, + } = resources; const annotationsClient = observability ? await withApmSpan('get_scoped_annotations_client', () => - observability.getScopedAnnotationsClient(context, request) + observability.setup.getScopedAnnotationsClient(context, request) ) : undefined; @@ -263,7 +274,7 @@ export const serviceAnnotationsCreateRoute = createRoute({ throw Boom.notFound(); } - const { body, path } = context.params; + const { body, path } = params; return withApmSpan('create_annotation', () => annotationsClient.create({ @@ -283,7 +294,7 @@ export const serviceAnnotationsCreateRoute = createRoute({ }, }); -export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ +const serviceErrorGroupsPrimaryStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', params: t.type({ @@ -300,13 +311,14 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { path: { serviceName }, query: { kuery, transactionType, environment }, - } = context.params; + } = params; return getServiceErrorGroupPrimaryStatistics({ kuery, serviceName, @@ -317,7 +329,7 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ }, }); -export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ +const serviceErrorGroupsComparisonStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', params: t.type({ @@ -337,8 +349,9 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { path: { serviceName }, @@ -351,7 +364,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ comparisonStart, comparisonEnd, }, - } = context.params; + } = params; return getServiceErrorGroupPeriods({ environment, @@ -367,7 +380,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ }, }); -export const serviceThroughputRoute = createRoute({ +const serviceThroughputRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/throughput', params: t.type({ path: t.type({ @@ -382,16 +395,17 @@ export const serviceThroughputRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const { environment, kuery, transactionType, comparisonStart, comparisonEnd, - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -432,7 +446,7 @@ export const serviceThroughputRoute = createRoute({ }, }); -export const serviceInstancesPrimaryStatisticsRoute = createRoute({ +const serviceInstancesPrimaryStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics', params: t.type({ @@ -450,12 +464,16 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { environment, kuery, transactionType } = context.params.query; - const latencyAggregationType = (context.params.query - .latencyAggregationType as unknown) as LatencyAggregationType; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + const { + environment, + kuery, + transactionType, + latencyAggregationType, + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -479,7 +497,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({ }, }); -export const serviceInstancesComparisonStatisticsRoute = createRoute({ +const serviceInstancesComparisonStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics', params: t.type({ @@ -500,9 +518,10 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const { environment, kuery, @@ -511,9 +530,8 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({ comparisonEnd, serviceNodeIds, numBuckets, - } = context.params.query; - const latencyAggregationType = (context.params.query - .latencyAggregationType as unknown) as LatencyAggregationType; + latencyAggregationType, + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -535,7 +553,7 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({ }, }); -export const serviceDependenciesRoute = createRoute({ +const serviceDependenciesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/dependencies', params: t.type({ path: t.type({ @@ -552,11 +570,11 @@ export const serviceDependenciesRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - - const { serviceName } = context.params.path; - const { environment, numBuckets } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + const { environment, numBuckets } = params.query; const serviceDependencies = await getServiceDependencies({ serviceName, @@ -569,7 +587,7 @@ export const serviceDependenciesRoute = createRoute({ }, }); -export const serviceProfilingTimelineRoute = createRoute({ +const serviceProfilingTimelineRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline', params: t.type({ path: t.type({ @@ -580,13 +598,13 @@ export const serviceProfilingTimelineRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; const { path: { serviceName }, query: { environment, kuery }, - } = context.params; + } = params; const profilingTimeline = await getServiceProfilingTimeline({ kuery, @@ -599,7 +617,7 @@ export const serviceProfilingTimelineRoute = createRoute({ }, }); -export const serviceProfilingStatisticsRoute = createRoute({ +const serviceProfilingStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics', params: t.type({ path: t.type({ @@ -625,13 +643,15 @@ export const serviceProfilingStatisticsRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params, logger } = resources; const { path: { serviceName }, query: { environment, kuery, valueType }, - } = context.params; + } = params; return getServiceProfilingStatistics({ kuery, @@ -639,7 +659,25 @@ export const serviceProfilingStatisticsRoute = createRoute({ environment, valueType, setup, - logger: context.logger, + logger, }); }, }); + +export const serviceRouteRepository = createApmServerRouteRepository() + .add(servicesRoute) + .add(serviceMetadataDetailsRoute) + .add(serviceMetadataIconsRoute) + .add(serviceAgentNameRoute) + .add(serviceTransactionTypesRoute) + .add(serviceNodeMetadataRoute) + .add(serviceAnnotationsRoute) + .add(serviceAnnotationsCreateRoute) + .add(serviceErrorGroupsPrimaryStatisticsRoute) + .add(serviceErrorGroupsComparisonStatisticsRoute) + .add(serviceThroughputRoute) + .add(serviceInstancesPrimaryStatisticsRoute) + .add(serviceInstancesComparisonStatisticsRoute) + .add(serviceDependenciesRoute) + .add(serviceProfilingTimelineRoute) + .add(serviceProfilingStatisticsRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 31e8d6cc1e9f0..111e0a18c8608 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -16,7 +16,7 @@ import { findExactConfiguration } from '../../lib/settings/agent_configuration/f import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations'; import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments'; import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service'; import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent'; import { @@ -24,34 +24,37 @@ import { agentConfigurationIntakeRt, } from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; // get list of configurations -export const agentConfigurationRoute = createRoute({ +const agentConfigurationRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const configurations = await listConfigurations({ setup }); return { configurations }; }, }); // get a single configuration -export const getSingleAgentConfigurationRoute = createRoute({ +const getSingleAgentConfigurationRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/view', params: t.partial({ query: serviceRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { name, environment } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + + const { name, environment } = params.query; const service = { name, environment }; const config = await findExactConfiguration({ service, setup }); if (!config) { - context.logger.info( + logger.info( `Config was not found for ${service.name}/${service.environment}` ); @@ -63,7 +66,7 @@ export const getSingleAgentConfigurationRoute = createRoute({ }); // delete configuration -export const deleteAgentConfigurationRoute = createRoute({ +const deleteAgentConfigurationRoute = createApmServerRoute({ endpoint: 'DELETE /api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'], @@ -73,20 +76,22 @@ export const deleteAgentConfigurationRoute = createRoute({ service: serviceRt, }), }), - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { service } = context.params.body; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + + const { service } = params.body; const config = await findExactConfiguration({ service, setup }); if (!config) { - context.logger.info( + logger.info( `Config was not found for ${service.name}/${service.environment}` ); throw Boom.notFound(); } - context.logger.info( + logger.info( `Deleting config ${service.name}/${service.environment} (${config._id})` ); @@ -98,7 +103,7 @@ export const deleteAgentConfigurationRoute = createRoute({ }); // create/update configuration -export const createOrUpdateAgentConfigurationRoute = createRoute({ +const createOrUpdateAgentConfigurationRoute = createApmServerRoute({ endpoint: 'PUT /api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'], @@ -107,9 +112,10 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({ t.partial({ query: t.partial({ overwrite: toBooleanRt }) }), t.type({ body: agentConfigurationIntakeRt }), ]), - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { body, query } = context.params; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + const { body, query } = params; // if the config already exists, it is fetched and updated // this is to avoid creating two configs with identical service params @@ -125,13 +131,13 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({ ); } - context.logger.info( + logger.info( `${config ? 'Updating' : 'Creating'} config ${body.service.name}/${ body.service.environment }` ); - return await createOrUpdateConfiguration({ + await createOrUpdateConfiguration({ configurationId: config?._id, configurationIntake: body, setup, @@ -147,35 +153,35 @@ const searchParamsRt = t.intersection([ export type AgentConfigSearchParams = t.TypeOf; // Lookup single configuration (used by APM Server) -export const agentConfigurationSearchRoute = createRoute({ +const agentConfigurationSearchRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/agent-configuration/search', params: t.type({ body: searchParamsRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { params, logger } = resources; + const { service, etag, mark_as_applied_by_agent: markAsAppliedByAgent, - } = context.params.body; + } = params.body; - const setup = await setupRequest(context, request); + const setup = await setupRequest(resources); const config = await searchConfigurations({ service, setup, }); if (!config) { - context.logger.debug( + logger.debug( `[Central configuration] Config was not found for ${service.name}/${service.environment}` ); throw Boom.notFound(); } - context.logger.info( - `Config was found for ${service.name}/${service.environment}` - ); + logger.info(`Config was found for ${service.name}/${service.environment}`); // update `applied_by_agent` field // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags) @@ -197,11 +203,11 @@ export const agentConfigurationSearchRoute = createRoute({ */ // get list of services -export const listAgentConfigurationServicesRoute = createRoute({ +const listAgentConfigurationServicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/services', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -215,15 +221,17 @@ export const listAgentConfigurationServicesRoute = createRoute({ }); // get environments for service -export const listAgentConfigurationEnvironmentsRoute = createRoute({ +const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/environments', params: t.partial({ query: t.partial({ serviceName: t.string }), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { serviceName } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -239,16 +247,27 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({ }); // get agentName for service -export const agentConfigurationAgentNameRoute = createRoute({ +const agentConfigurationAgentNameRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/agent_name', params: t.type({ query: t.type({ serviceName: t.string }), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.query; const agentName = await getAgentNameByService({ serviceName, setup }); return { agentName }; }, }); + +export const agentConfigurationRouteRepository = createApmServerRouteRepository() + .add(agentConfigurationRoute) + .add(getSingleAgentConfigurationRoute) + .add(deleteAgentConfigurationRoute) + .add(createOrUpdateAgentConfigurationRoute) + .add(agentConfigurationSearchRoute) + .add(listAgentConfigurationServicesRoute) + .add(listAgentConfigurationEnvironmentsRoute) + .add(agentConfigurationAgentNameRoute); 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 de7f35c4081bc..98467e1a4a0dd 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; @@ -18,15 +18,17 @@ import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; import { notifyFeatureUsage } from '../../feature'; import { withApmSpan } from '../../utils/with_apm_span'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; // get ML anomaly detection jobs for each environment -export const anomalyDetectionJobsRoute = createRoute({ +const anomalyDetectionJobsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:ml:canGetJobs'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { context, logger } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); @@ -34,7 +36,7 @@ export const anomalyDetectionJobsRoute = createRoute({ const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () => Promise.all([ - getAnomalyDetectionJobs(setup, context.logger), + getAnomalyDetectionJobs(setup, logger), hasLegacyJobs(setup), ]) ); @@ -47,7 +49,7 @@ export const anomalyDetectionJobsRoute = createRoute({ }); // create new ML anomaly detection jobs for each given environment -export const createAnomalyDetectionJobsRoute = createRoute({ +const createAnomalyDetectionJobsRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'], @@ -57,15 +59,17 @@ export const createAnomalyDetectionJobsRoute = createRoute({ environments: t.array(t.string), }), }), - handler: async ({ context, request }) => { - const { environments } = context.params.body; - const setup = await setupRequest(context, request); + handler: async (resources) => { + const { params, context, logger } = resources; + const { environments } = params.body; + + const setup = await setupRequest(resources); if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } - await createAnomalyDetectionJobs(setup, environments, context.logger); + await createAnomalyDetectionJobs(setup, environments, logger); notifyFeatureUsage({ licensingPlugin: context.licensing, @@ -77,11 +81,11 @@ export const createAnomalyDetectionJobsRoute = createRoute({ }); // get all available environments to create anomaly detection jobs for -export const anomalyDetectionEnvironmentsRoute = createRoute({ +const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/anomaly-detection/environments', options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -96,3 +100,8 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({ return { environments }; }, }); + +export const anomalyDetectionRouteRepository = createApmServerRouteRepository() + .add(anomalyDetectionJobsRoute) + .add(createAnomalyDetectionJobsRoute) + .add(anomalyDetectionEnvironmentsRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 91057c97579e4..003471aa89f39 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -6,7 +6,8 @@ */ import * as t from 'io-ts'; -import { createRoute } from '../create_route'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRoute } from '../create_apm_server_route'; import { getApmIndices, getApmIndexSettings, @@ -14,29 +15,30 @@ import { import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices'; // get list of apm indices and values -export const apmIndexSettingsRoute = createRoute({ +const apmIndexSettingsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/apm-index-settings', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { - const apmIndexSettings = await getApmIndexSettings({ context }); + handler: async ({ config, context }) => { + const apmIndexSettings = await getApmIndexSettings({ config, context }); return { apmIndexSettings }; }, }); // get apm indices configuration object -export const apmIndicesRoute = createRoute({ +const apmIndicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/apm-indices', options: { tags: ['access:apm'] }, - handler: async ({ context }) => { + handler: async (resources) => { + const { context, config } = resources; return await getApmIndices({ savedObjectsClient: context.core.savedObjects.client, - config: context.config, + config, }); }, }); // save ui indices -export const saveApmIndicesRoute = createRoute({ +const saveApmIndicesRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/apm-indices/save', options: { tags: ['access:apm', 'access:apm_write'], @@ -53,9 +55,15 @@ export const saveApmIndicesRoute = createRoute({ /* eslint-enable @typescript-eslint/naming-convention */ }), }), - handler: async ({ context }) => { - const { body } = context.params; + handler: async (resources) => { + const { params, context } = resources; + const { body } = params; const savedObjectsClient = context.core.savedObjects.client; return await saveApmIndices(savedObjectsClient, body); }, }); + +export const apmIndicesRouteRepository = createApmServerRouteRepository() + .add(apmIndexSettingsRoute) + .add(apmIndicesRoute) + .add(saveApmIndicesRoute); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index a6ab553f09419..c9c5d236c14f9 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -21,35 +21,40 @@ import { import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link'; import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; -import { createRoute } from '../create_route'; +import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; -export const customLinkTransactionRoute = createRoute({ +const customLinkTransactionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/custom_links/transaction', options: { tags: ['access:apm'] }, params: t.partial({ query: filterOptionsRt, }), - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { query } = context.params; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { query } = params; // picks only the items listed in FILTER_OPTIONS const filters = pick(query, FILTER_OPTIONS); return await getTransaction({ setup, filters }); }, }); -export const listCustomLinksRoute = createRoute({ +const listCustomLinksRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/custom_links', options: { tags: ['access:apm'] }, params: t.partial({ query: filterOptionsRt, }), - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const { query } = context.params; + const setup = await setupRequest(resources); + + const { query } = params; + // picks only the items listed in FILTER_OPTIONS const filters = pick(query, FILTER_OPTIONS); const customLinks = await listCustomLinks({ setup, filters }); @@ -57,29 +62,30 @@ export const listCustomLinksRoute = createRoute({ }, }); -export const createCustomLinkRoute = createRoute({ +const createCustomLinkRoute = createApmServerRoute({ endpoint: 'POST /api/apm/settings/custom_links', params: t.type({ body: payloadRt, }), options: { tags: ['access:apm', 'access:apm_write'] }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const customLink = context.params.body; - const res = await createOrUpdateCustomLink({ customLink, setup }); + const setup = await setupRequest(resources); + const customLink = params.body; notifyFeatureUsage({ licensingPlugin: context.licensing, featureName: 'customLinks', }); - return res; + + await createOrUpdateCustomLink({ customLink, setup }); }, }); -export const updateCustomLinkRoute = createRoute({ +const updateCustomLinkRoute = createApmServerRoute({ endpoint: 'PUT /api/apm/settings/custom_links/{id}', params: t.type({ path: t.type({ @@ -90,23 +96,26 @@ export const updateCustomLinkRoute = createRoute({ options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { params, context } = resources; + if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const { id } = context.params.path; - const customLink = context.params.body; - const res = await createOrUpdateCustomLink({ + const setup = await setupRequest(resources); + + const { id } = params.path; + const customLink = params.body; + + await createOrUpdateCustomLink({ customLinkId: id, customLink, setup, }); - return res; }, }); -export const deleteCustomLinkRoute = createRoute({ +const deleteCustomLinkRoute = createApmServerRoute({ endpoint: 'DELETE /api/apm/settings/custom_links/{id}', params: t.type({ path: t.type({ @@ -116,12 +125,14 @@ export const deleteCustomLinkRoute = createRoute({ options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async ({ context, request }) => { + handler: async (resources) => { + const { context, params } = resources; + if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); } - const setup = await setupRequest(context, request); - const { id } = context.params.path; + const setup = await setupRequest(resources); + const { id } = params.path; const res = await deleteCustomLink({ customLinkId: id, setup, @@ -129,3 +140,10 @@ export const deleteCustomLinkRoute = createRoute({ return res; }, }); + +export const customLinkRouteRepository = createApmServerRouteRepository() + .add(customLinkTransactionRoute) + .add(listCustomLinksRoute) + .add(createCustomLinkRoute) + .add(updateCustomLinkRoute) + .add(deleteCustomLinkRoute); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 6287ffbf0c751..dd392982b02fd 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -9,20 +9,22 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getTrace } from '../lib/traces/get_trace'; import { getTransactionGroupList } from '../lib/transaction_groups'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -export const tracesRoute = createRoute({ +const tracesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces', params: t.type({ query: t.intersection([environmentRt, kueryRt, rangeRt]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { environment, kuery } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { environment, kuery } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); @@ -34,7 +36,7 @@ export const tracesRoute = createRoute({ }, }); -export const tracesByIdRoute = createRoute({ +const tracesByIdRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces/{traceId}', params: t.type({ path: t.type({ @@ -43,13 +45,16 @@ export const tracesByIdRoute = createRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - return getTrace(context.params.path.traceId, setup); + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { traceId } = params.path; + return getTrace(traceId, setup); }, }); -export const rootTransactionByTraceIdRoute = createRoute({ +const rootTransactionByTraceIdRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces/{traceId}/root_transaction', params: t.type({ path: t.type({ @@ -57,9 +62,15 @@ export const rootTransactionByTraceIdRoute = createRoute({ }), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const { traceId } = context.params.path; - const setup = await setupRequest(context, request); + handler: async (resources) => { + const { params } = resources; + const { traceId } = params.path; + const setup = await setupRequest(resources); return getRootTransactionByTraceId(traceId, setup); }, }); + +export const traceRouteRepository = createApmServerRouteRepository() + .add(tracesByIdRoute) + .add(tracesRoute) + .add(rootTransactionByTraceIdRoute); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index f3424a252e409..ebca374db86d7 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { LatencyAggregationType, latencyAggregationTypeRt, } from '../../common/latency_aggregation_types'; -import { jsonRt } from '../../common/runtime_types/json_rt'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; @@ -23,7 +23,8 @@ import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; -import { createRoute } from './create_route'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { comparisonRangeRt, environmentRt, @@ -35,7 +36,7 @@ import { * Returns a list of transactions grouped by name * //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/ */ -export const transactionGroupsRoute = createRoute({ +const transactionGroupsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups', params: t.type({ path: t.type({ @@ -49,10 +50,11 @@ export const transactionGroupsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { environment, kuery, transactionType } = context.params.query; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; + const { environment, kuery, transactionType } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -72,7 +74,7 @@ export const transactionGroupsRoute = createRoute({ }, }); -export const transactionGroupsPrimaryStatisticsRoute = createRoute({ +const transactionGroupsPrimaryStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics', params: t.type({ @@ -90,8 +92,9 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const { params } = resources; + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -100,7 +103,7 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ const { path: { serviceName }, query: { environment, kuery, latencyAggregationType, transactionType }, - } = context.params; + } = params; return getServiceTransactionGroups({ environment, @@ -109,12 +112,12 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ serviceName, searchAggregatedTransactions, transactionType, - latencyAggregationType: latencyAggregationType as LatencyAggregationType, + latencyAggregationType, }); }, }); -export const transactionGroupsComparisonStatisticsRoute = createRoute({ +const transactionGroupsComparisonStatisticsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics', params: t.type({ @@ -135,13 +138,15 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); + const { params } = resources; + const { path: { serviceName }, query: { @@ -154,7 +159,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ comparisonStart, comparisonEnd, }, - } = context.params; + } = params; return await getServiceTransactionGroupComparisonStatisticsPeriods({ environment, @@ -165,14 +170,14 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ searchAggregatedTransactions, transactionType, numBuckets, - latencyAggregationType: latencyAggregationType as LatencyAggregationType, + latencyAggregationType, comparisonStart, comparisonEnd, }); }, }); -export const transactionLatencyChartsRoute = createRoute({ +const transactionLatencyChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency', params: t.type({ path: t.type({ @@ -188,10 +193,11 @@ export const transactionLatencyChartsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const logger = context.logger; - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params, logger } = resources; + + const { serviceName } = params.path; const { environment, kuery, @@ -200,7 +206,7 @@ export const transactionLatencyChartsRoute = createRoute({ latencyAggregationType, comparisonStart, comparisonEnd, - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -242,7 +248,7 @@ export const transactionLatencyChartsRoute = createRoute({ }, }); -export const transactionThroughputChartsRoute = createRoute({ +const transactionThroughputChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/throughput', params: t.type({ @@ -258,15 +264,17 @@ export const transactionThroughputChartsRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { serviceName } = params.path; const { environment, kuery, transactionType, transactionName, - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -284,7 +292,7 @@ export const transactionThroughputChartsRoute = createRoute({ }, }); -export const transactionChartsDistributionRoute = createRoute({ +const transactionChartsDistributionRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', params: t.type({ @@ -306,9 +314,10 @@ export const transactionChartsDistributionRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { serviceName } = params.path; const { environment, kuery, @@ -316,7 +325,7 @@ export const transactionChartsDistributionRoute = createRoute({ transactionName, transactionId = '', traceId = '', - } = context.params.query; + } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -336,7 +345,7 @@ export const transactionChartsDistributionRoute = createRoute({ }, }); -export const transactionChartsBreakdownRoute = createRoute({ +const transactionChartsBreakdownRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown', params: t.type({ path: t.type({ @@ -351,15 +360,17 @@ export const transactionChartsBreakdownRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { serviceName } = params.path; const { environment, kuery, transactionName, transactionType, - } = context.params.query; + } = params.query; return getTransactionBreakdown({ environment, @@ -372,7 +383,7 @@ export const transactionChartsBreakdownRoute = createRoute({ }, }); -export const transactionChartsErrorRateRoute = createRoute({ +const transactionChartsErrorRateRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/error_rate', params: t.type({ @@ -386,9 +397,10 @@ export const transactionChartsErrorRateRoute = createRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { params } = context; + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params } = resources; const { serviceName } = params.path; const { environment, @@ -416,3 +428,13 @@ export const transactionChartsErrorRateRoute = createRoute({ }); }, }); + +export const transactionRouteRepository = createApmServerRouteRepository() + .add(transactionGroupsRoute) + .add(transactionGroupsPrimaryStatisticsRoute) + .add(transactionGroupsComparisonStatisticsRoute) + .add(transactionLatencyChartsRoute) + .add(transactionThroughputChartsRoute) + .add(transactionChartsDistributionRoute) + .add(transactionChartsBreakdownRoute) + .add(transactionChartsErrorRateRoute); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 3ba24b4ed5268..0fec88a4326c3 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -5,27 +5,19 @@ * 2.0. */ -import t, { Encode, Encoder } from 'io-ts'; import { CoreSetup, - KibanaRequest, RequestHandlerContext, Logger, + KibanaRequest, + CoreStart, } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { RequiredKeys, DeepPartial } from 'utility-types'; -import { SpacesPluginStart } from '../../../spaces/server'; -import { ObservabilityPluginSetup } from '../../../observability/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; -import { SecurityPluginSetup } from '../../../security/server'; -import { MlPluginSetup } from '../../../ml/server'; -import { FetchOptions } from '../../common/fetch_options'; import { APMConfig } from '..'; +import { APMPluginDependencies } from '../types'; -export type HandlerReturn = Record; - -interface InspectQueryParam { - query: { _inspect: boolean }; +export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { + licensing: LicensingApiRequestHandlerContext; } export type InspectResponse = Array<{ @@ -36,141 +28,53 @@ export type InspectResponse = Array<{ esError: Error; }>; -export interface RouteParams { - path?: Record; - query?: Record; - body?: any; +export interface APMRouteCreateOptions { + options: { + tags: Array< + | 'access:apm' + | 'access:apm_write' + | 'access:ml:canGetJobs' + | 'access:ml:canCreateJob' + >; + }; } -type WithoutIncompatibleMethods = Omit< - T, - 'encode' | 'asEncoder' -> & { encode: Encode; asEncoder: () => Encoder }; - -export type RouteParamsRT = WithoutIncompatibleMethods>; - -export type RouteHandler< - TParamsRT extends RouteParamsRT | undefined, - TReturn extends HandlerReturn -> = (kibanaContext: { - context: APMRequestHandlerContext< - (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) & - InspectQueryParam - >; +export interface APMRouteHandlerResources { request: KibanaRequest; -}) => Promise; - -interface RouteOptions { - tags: Array< - | 'access:apm' - | 'access:apm_write' - | 'access:ml:canGetJobs' - | 'access:ml:canCreateJob' - >; -} - -export interface Route< - TEndpoint extends string, - TRouteParamsRT extends RouteParamsRT | undefined, - TReturn extends HandlerReturn -> { - endpoint: TEndpoint; - options: RouteOptions; - params?: TRouteParamsRT; - handler: RouteHandler; -} - -/** - * @internal - */ -export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { - licensing: LicensingApiRequestHandlerContext; -} - -export type APMRequestHandlerContext< - TRouteParams = {} -> = ApmPluginRequestHandlerContext & { - params: TRouteParams & InspectQueryParam; + context: ApmPluginRequestHandlerContext; + params: { + query: { + _inspect: boolean; + }; + }; config: APMConfig; logger: Logger; - plugins: { - spaces?: SpacesPluginStart; - observability?: ObservabilityPluginSetup; - security?: SecurityPluginSetup; - ml?: MlPluginSetup; + core: { + setup: CoreSetup; + start: () => Promise; }; -}; - -export interface RouteState { - [endpoint: string]: { - params?: RouteParams; - ret: any; + plugins: { + [key in keyof APMPluginDependencies]: { + setup: Required[key]['setup']; + start: () => Promise[key]['start']>; + }; }; } -export interface ServerAPI { - _S: TRouteState; - add< - TEndpoint extends string, - TReturn extends HandlerReturn, - TRouteParamsRT extends RouteParamsRT | undefined = undefined - >( - route: - | Route - | ((core: CoreSetup) => Route) - ): ServerAPI< - TRouteState & - { - [key in TEndpoint]: { - params: TRouteParamsRT; - ret: TReturn & { _inspect?: InspectResponse }; - }; - } - >; - init: ( - core: CoreSetup, - context: { - config$: Observable; - logger: Logger; - plugins: { - observability?: ObservabilityPluginSetup; - security?: SecurityPluginSetup; - ml?: MlPluginSetup; - }; - } - ) => void; -} - -type MaybeOptional }> = RequiredKeys< - T['params'] -> extends never - ? { params?: T['params'] } - : { params: T['params'] }; - -export type MaybeParams< - TRouteState, - TEndpoint extends keyof TRouteState & string -> = TRouteState[TEndpoint] extends { params: t.Any } - ? MaybeOptional<{ - params: t.OutputOf & - DeepPartial; - }> - : {}; - -export type Client< - TRouteState, - TOptions extends { abortable: boolean } = { abortable: true } -> = ( - options: Omit< - FetchOptions, - 'query' | 'body' | 'pathname' | 'method' | 'signal' - > & { - forceCache?: boolean; - endpoint: TEndpoint; - } & MaybeParams & - (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {}) -) => Promise< - TRouteState[TEndpoint] extends { ret: any } - ? TRouteState[TEndpoint]['ret'] - : unknown ->; +// export type Client< +// TRouteState, +// TOptions extends { abortable: boolean } = { abortable: true } +// > = ( +// options: Omit< +// FetchOptions, +// 'query' | 'body' | 'pathname' | 'method' | 'signal' +// > & { +// forceCache?: boolean; +// endpoint: TEndpoint; +// } & MaybeParams & +// (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {}) +// ) => Promise< +// TRouteState[TEndpoint] extends { ret: any } +// ? TRouteState[TEndpoint]['ret'] +// : unknown +// >; diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts new file mode 100644 index 0000000000000..cef9eaf2f4fc0 --- /dev/null +++ b/x-pack/plugins/apm/server/types.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ValuesType } from 'utility-types'; +import { Observable } from 'rxjs'; +import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; +import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; +import { + HomeServerPluginSetup, + HomeServerPluginStart, +} from '../../../../src/plugins/home/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { ActionsPlugin } from '../../actions/server'; +import { AlertingPlugin } from '../../alerting/server'; +import { CloudSetup } from '../../cloud/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; +import { + LicensingPluginSetup, + LicensingPluginStart, +} from '../../licensing/server'; +import { MlPluginSetup, MlPluginStart } from '../../ml/server'; +import { ObservabilityPluginSetup } from '../../observability/server'; +import { + SecurityPluginSetup, + SecurityPluginStart, +} from '../../security/server'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../task_manager/server'; +import { APMConfig } from '.'; +import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; +import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client'; +import { ApmPluginRequestHandlerContext } from './routes/typings'; + +export interface APMPluginSetup { + config$: Observable; + getApmIndices: () => ReturnType; + createApmEventClient: (params: { + debug?: boolean; + request: KibanaRequest; + context: ApmPluginRequestHandlerContext; + }) => Promise>; +} + +interface DependencyMap { + core: { + setup: CoreSetup; + start: CoreStart; + }; + spaces: { + setup: SpacesPluginSetup; + start: SpacesPluginStart; + }; + apmOss: { + setup: APMOSSPluginSetup; + start: undefined; + }; + home: { + setup: HomeServerPluginSetup; + start: HomeServerPluginStart; + }; + licensing: { + setup: LicensingPluginSetup; + start: LicensingPluginStart; + }; + cloud: { + setup: CloudSetup; + start: undefined; + }; + usageCollection: { + setup: UsageCollectionSetup; + start: undefined; + }; + taskManager: { + setup: TaskManagerSetupContract; + start: TaskManagerStartContract; + }; + alerting: { + setup: AlertingPlugin['setup']; + start: AlertingPlugin['start']; + }; + actions: { + setup: ActionsPlugin['setup']; + start: ActionsPlugin['start']; + }; + observability: { + setup: ObservabilityPluginSetup; + start: undefined; + }; + features: { + setup: FeaturesPluginSetup; + start: FeaturesPluginStart; + }; + security: { + setup: SecurityPluginSetup; + start: SecurityPluginStart; + }; + ml: { + setup: MlPluginSetup; + start: MlPluginStart; + }; + data: { + setup: DataPluginSetup; + start: DataPluginStart; + }; +} + +const requiredDependencies = [ + 'features', + 'apmOss', + 'data', + 'licensing', + 'triggersActionsUi', + 'embeddable', + 'infra', +] as const; + +const optionalDependencies = [ + 'spaces', + 'cloud', + 'usageCollection', + 'taskManager', + 'actions', + 'alerting', + 'observability', + 'security', + 'ml', + 'home', + 'maps', +] as const; + +type RequiredDependencies = Pick< + DependencyMap, + ValuesType & keyof DependencyMap +>; + +type OptionalDependencies = Partial< + Pick< + DependencyMap, + ValuesType & keyof DependencyMap + > +>; + +export type APMPluginDependencies = RequiredDependencies & OptionalDependencies; + +export type APMPluginSetupDependencies = { + [key in keyof APMPluginDependencies]: Required[key]['setup']; +}; + +export type APMPluginStartDependencies = { + [key in keyof APMPluginDependencies]: Required[key]['start']; +}; diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index ae2085dc24003..ffbf11c23f63a 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/banners/tsconfig.json b/x-pack/plugins/banners/tsconfig.json index 6c4c80173208b..85608a8a78ad5 100644 --- a/x-pack/plugins/banners/tsconfig.json +++ b/x-pack/plugins/banners/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/beats_management/tsconfig.json b/x-pack/plugins/beats_management/tsconfig.json index 398438712b26b..ad68cc900e638 100644 --- a/x-pack/plugins/beats_management/tsconfig.json +++ b/x-pack/plugins/beats_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index c14e8340957ad..cff1a3e7fa8b7 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -13,6 +13,7 @@ "expressions", "features", "inspector", + "presentationUtil", "uiActions" ], "optionalPlugins": [ diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 66b02bdc16408..f910aff9a83fe 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -56,17 +56,20 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const { presentationUtil } = plugins; element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); ReactDOM.render( - - - - - + + + + + + + , element diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss index 2ed6884542b18..620e0eb113d36 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss @@ -66,7 +66,7 @@ text-decoration: none; .canvasPageManager__pagePreview { - @include euiBottomShadowMedium($opacity: .3); + @include euiBottomShadowMedium; } .canvasPageManager__controls { diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss index 9266273406b84..e770f10927552 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss @@ -1,5 +1,5 @@ .canvasPage { - @include euiBottomShadowFlat($opacity: .4); + @include euiBottomShadowFlat; z-index: initial; position: absolute; top: 0; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 6871c8d98b8f5..486cd03eb9dd6 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -27,6 +27,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -51,6 +52,7 @@ export interface CanvasStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } /** diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 6e74b5ac98621..3865d98caf2b3 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -35,6 +35,7 @@ export const useEmbeddablesService = () => useServices().embeddables; export const useExpressionsService = () => useServices().expressions; export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; +export const useLabsService = () => useServices().labs; export const withServices = (type: ComponentType) => { const EnhancedType: FC = (props) => @@ -53,6 +54,7 @@ export const ServicesProvider: FC<{ notify: specifiedProviders.notify.getService(), platform: specifiedProviders.platform.getService(), navLink: specifiedProviders.navLink.getService(), + labs: specifiedProviders.labs.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 7452352fc0ef4..9bfc41a782edc 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -13,6 +13,7 @@ import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; +import { labsServiceFactory } from './labs'; export { NotifyService } from './notify'; export { PlatformService } from './platform'; @@ -78,6 +79,7 @@ export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), + labs: new CanvasServiceProvider(labsServiceFactory), }; export type CanvasServiceProviders = typeof services; @@ -88,6 +90,7 @@ export interface CanvasServices { notify: ServiceFromProvider; platform: ServiceFromProvider; navLink: ServiceFromProvider; + labs: ServiceFromProvider; } export const startServices = async ( diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/labs.ts new file mode 100644 index 0000000000000..9bc4bea3e35c3 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/labs.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + projectIDs, + Project, + ProjectID, +} from '../../../../../src/plugins/presentation_util/public'; + +import { CanvasServiceFactory } from '.'; + +export interface CanvasLabsService { + getProject: (id: ProjectID) => Project; + getProjects: () => Record; +} + +export const labsServiceFactory: CanvasServiceFactory = async ( + _coreSetup, + _coreStart, + _setupPlugins, + startPlugins +) => ({ + projectIDs, + ...startPlugins.presentationUtil.labsService, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 2565445af2db2..91bda2556284e 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -10,6 +10,7 @@ import { embeddablesService } from './embeddables'; import { expressionsService } from './expressions'; import { navLinkService } from './nav_link'; import { notifyService } from './notify'; +import { labsService } from './labs'; import { platformService } from './platform'; export const stubs: CanvasServices = { @@ -18,6 +19,7 @@ export const stubs: CanvasServices = { navLink: navLinkService, notify: notifyService, platform: platformService, + labs: labsService, }; export const startServices = async (providedServices: Partial = {}) => { diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts new file mode 100644 index 0000000000000..52168ebeb6f80 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/labs.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CanvasLabsService } from '../labs'; + +const noop = (..._args: any[]): any => {}; + +export const labsService: CanvasLabsService = { + getProject: noop, + getProjects: noop, +}; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index acf71cad3f3ba..b68642d184542 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,7 +59,8 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/95899 +describe.skip('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json index b115d1c46546c..2ab1856de661a 100644 --- a/x-pack/plugins/canvas/storybook/addon/tsconfig.json +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../../tsconfig.project.json", + "extends": "../../../../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx" diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 679165f0a1b76..487b68ba3542b 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index f6edb9fb7ccae..46e81aa7fa086 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/console_extensions/tsconfig.json b/x-pack/plugins/console_extensions/tsconfig.json index edcd46c4fafc5..5ad28f230a0bb 100644 --- a/x-pack/plugins/console_extensions/tsconfig.json +++ b/x-pack/plugins/console_extensions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json index 156a851abb8db..9c7590b9c2553 100644 --- a/x-pack/plugins/cross_cluster_replication/tsconfig.json +++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index f6acdddc6f997..567c390edfa5a 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/dashboard_mode/tsconfig.json b/x-pack/plugins/dashboard_mode/tsconfig.json index c4a11959ec3e3..6e4ed11ffa7ff 100644 --- a/x-pack/plugins/dashboard_mode/tsconfig.json +++ b/x-pack/plugins/dashboard_mode/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index fc1f22d50b09f..8cbf930fe87bd 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -18,7 +18,7 @@ export const configSchema = schema.object({ * pageSize controls how many search session objects we load at once while monitoring * session completion */ - pageSize: schema.number({ defaultValue: 10000 }), + pageSize: schema.number({ defaultValue: 100 }), /** * trackingInterval controls how often we track search session objects progress */ diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 462d1fc337ae2..ae36b881796c4 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,13 +6,7 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { - PluginSetup as DataPluginSetup, - PluginStart as DataPluginStart, - usageProvider, -} from '../../../../src/plugins/data/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { usageProvider } from '../../../../src/plugins/data/server'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; import { registerSessionRoutes } from './routes'; import { searchSessionSavedObjectType } from './saved_objects'; @@ -22,22 +16,13 @@ import { eqlSearchStrategyProvider, } from './search'; import { getUiSettings } from './ui_settings'; -import type { DataEnhancedRequestHandlerContext } from './type'; +import type { + DataEnhancedRequestHandlerContext, + DataEnhancedSetupDependencies as SetupDependencies, + DataEnhancedStartDependencies as StartDependencies, +} from './type'; import { ConfigSchema } from '../config'; import { registerUsageCollector } from './collectors'; -import { SecurityPluginSetup } from '../../security/server'; - -interface SetupDependencies { - data: DataPluginSetup; - usageCollection?: UsageCollectionSetup; - taskManager: TaskManagerSetupContract; - security?: SecurityPluginSetup; -} - -export interface StartDependencies { - data: DataPluginStart; - taskManager: TaskManagerStartContract; -} export class EnhancedDataServerPlugin implements Plugin { @@ -50,7 +35,7 @@ export class EnhancedDataServerPlugin this.config = this.initializerContext.config.get(); } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { const usage = deps.usageCollection ? usageProvider(core) : undefined; core.uiSettings.register(getUiSettings()); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index 6e52b17f36803..60c7283320d0c 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; -import { expand, mergeMap } from 'rxjs/operators'; +import { expand, concatMap } from 'rxjs/operators'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { ENHANCED_ES_SEARCH_STRATEGY, @@ -154,7 +154,7 @@ export async function checkRunningSessions( try { await getAllSavedSearchSessions$(deps, config) .pipe( - mergeMap(async (runningSearchSessionsResponse) => { + concatMap(async (runningSearchSessionsResponse) => { if (!runningSearchSessionsResponse.total) return; logger.debug(`Found ${runningSearchSessionsResponse.total} running sessions`); diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 8aa35def387b7..101ccb14edf67 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -15,6 +15,7 @@ import { checkRunningSessions } from './check_running_sessions'; import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; import { ConfigSchema } from '../../../config'; import { SEARCH_SESSION_TYPE } from '../../../common'; +import { DataEnhancedStartDependencies } from '../../type'; export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; @@ -25,12 +26,19 @@ interface SearchSessionTaskDeps { config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { +function searchSessionRunner( + core: CoreSetup, + { logger, config }: SearchSessionTaskDeps +) { return ({ taskInstance }: RunContext) => { return { async run() { const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); + if (!sessionConfig.enabled) { + logger.debug('Search sessions are disabled. Skipping task.'); + return; + } const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); await checkRunningSessions( @@ -50,7 +58,10 @@ function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionT }; } -export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionTaskDeps) { +export function registerSearchSessionsTask( + core: CoreSetup, + deps: SearchSessionTaskDeps +) { deps.taskManager.registerTaskDefinitions({ [SEARCH_SESSIONS_TASK_TYPE]: { title: 'Search Sessions Monitor', @@ -59,6 +70,18 @@ export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionT }); } +export async function unscheduleSearchSessionsTask( + taskManager: TaskManagerStartContract, + logger: Logger +) { + try { + await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); + logger.debug(`Search sessions cleared`); + } catch (e) { + logger.error(`Error clearing task, received ${e.message}`); + } +} + export async function scheduleSearchSessionsTasks( taskManager: TaskManagerStartContract, logger: Logger, @@ -79,6 +102,6 @@ export async function scheduleSearchSessionsTasks( logger.debug(`Search sessions task, scheduled to run`); } catch (e) { - logger.debug(`Error scheduling task, received ${e.message}`); + logger.error(`Error scheduling task, received ${e.message}`); } } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index f61d89e2301ab..9344ab973c636 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -15,12 +15,12 @@ import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../common'; import { SearchSessionService } from './session_service'; import { createRequestHash } from './utils'; import moment from 'moment'; -import { coreMock } from 'src/core/server/mocks'; +import { coreMock } from '../../../../../../src/core/server/mocks'; import { ConfigSchema } from '../../../config'; -// @ts-ignore import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { AuthenticatedUser } from '../../../../security/common/model'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; +import { TaskManagerStartContract } from '../../../../task_manager/server'; const MAX_UPDATE_RETRIES = 3; @@ -29,6 +29,7 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); describe('SearchSessionService', () => { let savedObjectsClient: jest.Mocked; let service: SearchSessionService; + let mockTaskManager: jest.Mocked; const MOCK_STRATEGY = 'ese'; @@ -62,925 +63,1009 @@ describe('SearchSessionService', () => { references: [], }; - beforeEach(async () => { - savedObjectsClient = savedObjectsClientMock.create(); - const config: ConfigSchema = { - search: { - sessions: { - enabled: true, - pageSize: 10000, - notTouchedInProgressTimeout: moment.duration(1, 'm'), - notTouchedTimeout: moment.duration(2, 'm'), - maxUpdateRetries: MAX_UPDATE_RETRIES, - defaultExpiration: moment.duration(7, 'd'), - trackingInterval: moment.duration(10, 's'), - management: {} as any, + describe('Feature disabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: false, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, + }, }, - }, - }; - const mockLogger: any = { - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - service = new SearchSessionService(mockLogger, config); - const coreStart = coreMock.createStart(); - const mockTaskManager = taskManagerMock.createStart(); - await flushPromises(); - await service.start(coreStart, { - taskManager: mockTaskManager, - }); - }); - - afterEach(() => { - service.stop(); - }); - - describe('save', () => { - it('throws if `name` is not provided', () => { - expect(() => - service.save({ savedObjectsClient }, mockUser1, sessionId, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('throws if `appId` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) - ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + afterEach(() => { + service.stop(); }); - it('throws if `generator id` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - }) - ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + it('task is cleared, if exists', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); }); - it('saving updates an existing saved object and persists it', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', + it('trackId ignores', async () => { + await service.trackId({ savedObjectsClient }, mockUser1, { params: {} }, '123', { + sessionId: '321', + strategy: MOCK_STRATEGY, }); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); expect(savedObjectsClient.create).not.toHaveBeenCalled(); - - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).not.toHaveProperty('idMapping'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); }); - it('saving creates a new persisted saved object, if it did not exist', async () => { - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', - }); - - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options?.id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', {}); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); - expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); - expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); - expect(callAttributes).toHaveProperty('username', mockUser1.username); + it('Save throws', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toBeInstanceOf(Error); }); - it('throws error if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - expect( - service.get({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + it('Update throws', () => { + const attributes = { name: 'new_name' }; + const response = service.update({ savedObjectsClient }, mockUser1, sessionId, attributes); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - - await service.save( - { savedObjectsClient }, - - null, - sessionId, - { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - } - ); - - expect(savedObjectsClient.create).toHaveBeenCalled(); - const [[, attributes]] = savedObjectsClient.create.mock.calls; - expect(attributes).toHaveProperty('realmType', undefined); - expect(attributes).toHaveProperty('realmName', undefined); - expect(attributes).toHaveProperty('username', undefined); + it('Cancel throws', () => { + const response = service.cancel({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); - }); - - describe('get', () => { - it('calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('getId throws', () => { + const response = service.getId({ savedObjectsClient }, mockUser1, {}, {}); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get({ savedObjectsClient }, null, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('Delete throws', () => { + const response = service.delete({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); }); - describe('find', () => { - it('calls saved objects client with user filter', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find({ savedObjectsClient }, mockUser1, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", + describe('Feature enabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: true, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, }, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); - }); - - it('mixes in passed-in filter as string and KQL node', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, + }, }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options1 = { filter: 'foobar' }; - const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); - - const options2 = { filter: nodeBuilder.is('foo', 'bar') }; - const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); - - expect(response1).toBe(mockResponse); - expect(response2).toBe(mockResponse); - - const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; - expect(findOptions1).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": null, - }, - Object { - "type": "literal", - "value": "foobar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); - expect(findOptions2).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "foo", - }, - Object { - "type": "literal", - "value": "bar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('has no filter without security', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find({ savedObjectsClient }, null, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": undefined, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); + afterEach(() => { + service.stop(); }); - }); - - describe('update', () => { - it('update calls saved objects client with added touch time', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - const response = await service.update( - { savedObjectsClient }, - mockUser1, - sessionId, - attributes - ); + it('task is cleared and re-created', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); + expect(mockTaskManager.ensureScheduled).toHaveBeenCalled(); + }); - expect(response).toBe(mockUpdateSavedObject); + describe('save', () => { + it('throws if `name` is not provided', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + it('throws if `appId` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) + ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', attributes.name); - expect(callAttributes).toHaveProperty('touched'); - }); + it('throws if `generator id` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + }) + ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + }); - it('throws if user conflicts', () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('saving updates an existing saved object and persists it', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - expect( - service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - it('works without security', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).not.toHaveProperty('idMapping'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + }); - const attributes = { name: 'new_name' }; - const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - - expect(response).toBe(mockUpdateSavedObject); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', 'new_name'); - expect(callAttributes).toHaveProperty('touched'); - }); - }); + it('saving creates a new persisted saved object, if it did not exist', async () => { + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - describe('cancel', () => { - it('updates object status', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - await service.cancel({ savedObjectsClient }, mockUser1, sessionId); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); - }); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options?.id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', {}); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); + expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); + expect(callAttributes).toHaveProperty('username', mockUser1.username); + }); - it('throws if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('throws error if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - expect( - service.cancel({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + expect( + service.get({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('works without security', async () => { + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); - await service.cancel({ savedObjectsClient }, null, sessionId); + await service.save( + { savedObjectsClient }, - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + null, + sessionId, + { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + } + ); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); + expect(savedObjectsClient.create).toHaveBeenCalled(); + const [[, attributes]] = savedObjectsClient.create.mock.calls; + expect(attributes).toHaveProperty('realmType', undefined); + expect(attributes).toHaveProperty('realmName', undefined); + expect(attributes).toHaveProperty('username', undefined); + }); }); - }); - describe('trackId', () => { - it('updates the saved object if search session already exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('get', () => { + it('calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + const response = await service.get({ savedObjectsClient }, null, sessionId); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(callAttributes).toHaveProperty('touched'); }); - it('retries updating the saved object if there was a ES conflict 409', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - let counter = 0; - - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - } else { - resolve(mockUpdateSavedObject); + describe('find', () => { + it('calls saved objects client with user filter', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find({ savedObjectsClient }, mockUser1, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "page": 0, + "perPage": 5, + "type": "search-session", } - }); + `); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('mixes in passed-in filter as string and KQL node', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options1 = { filter: 'foobar' }; + const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); + + const options2 = { filter: nodeBuilder.is('foo', 'bar') }; + const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); + + expect(response1).toBe(mockResponse); + expect(response2).toBe(mockResponse); + + const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; + expect(findOptions1).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": null, + }, + Object { + "type": "literal", + "value": "foobar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); + expect(findOptions2).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "foo", + }, + Object { + "type": "literal", + "value": "bar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('has no filter without security', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find({ savedObjectsClient }, null, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": undefined, + "page": 0, + "perPage": 5, + "type": "search-session", + } + `); + }); }); - it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('update', () => { + it('update calls saved objects client with added touch time', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update( + { savedObjectsClient }, + mockUser1, + sessionId, + attributes + ); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - }); + expect(response).toBe(mockUpdateSavedObject); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', attributes.name); + expect(callAttributes).toHaveProperty('touched'); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + expect( + service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - // Track ID doesn't throw errors even in cases of failure! - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(response).toBe(mockUpdateSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', 'new_name'); + expect(callAttributes).toHaveProperty('touched'); + }); }); - it('creates the saved object in non persisted state, if search session doesnt exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('cancel', () => { + it('updates object status', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + await service.cancel({ savedObjectsClient }, mockUser1, sessionId); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.cancel({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options).toStrictEqual({ id: sessionId }); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + await service.cancel({ savedObjectsClient }, null, sessionId); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); }); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('sessionId', sessionId); - expect(callAttributes).toHaveProperty('persisted', false); }); - it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('trackId', () => { + it('updates the saved object if search session already exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - let counter = 0; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); - } else { - resolve(mockUpdateSavedObject); - } + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, }); + expect(callAttributes).toHaveProperty('touched'); }); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + it('retries updating the saved object if there was a ES conflict 409', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - }); + it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('retries everything at most MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + }); + }); - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + // Track ID doesn't throw errors even in cases of failure! + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - }); + it('creates the saved object in non persisted state, if search session doesnt exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('batches updates for the same session', async () => { - const sessionId1 = 'sessiondId1'; - const sessionId2 = 'sessiondId2'; + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - const searchRequest1 = { params: { 1: '1' } }; - const requestHash1 = createRequestHash(searchRequest1.params); - const searchId1 = 'searchId1'; + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - const searchRequest2 = { params: { 2: '2' } }; - const requestHash2 = createRequestHash(searchRequest2.params); - const searchId2 = 'searchId1'; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - const searchRequest3 = { params: { 3: '3' } }; - const requestHash3 = createRequestHash(searchRequest3.params); - const searchId3 = 'searchId3'; + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options).toStrictEqual({ id: sessionId }); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('sessionId', sessionId); + expect(callAttributes).toHaveProperty('persisted', false); + }); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await Promise.all([ - service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { - sessionId: sessionId2, + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }), - ]); + }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + }); - const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; - expect(type1).toBe(SEARCH_SESSION_TYPE); - expect(id1).toBe(sessionId1); - expect(callAttributes1).toHaveProperty('idMapping', { - [requestHash1]: { - id: searchId1, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, - [requestHash2]: { - id: searchId2, - status: SearchSessionStatus.IN_PROGRESS, + it('retries everything at most MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); }); - expect(callAttributes1).toHaveProperty('touched'); - const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; - expect(type2).toBe(SEARCH_SESSION_TYPE); - expect(id2).toBe(sessionId2); - expect(callAttributes2).toHaveProperty('idMapping', { - [requestHash3]: { - id: searchId3, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + it('batches updates for the same session', async () => { + const sessionId1 = 'sessiondId1'; + const sessionId2 = 'sessiondId2'; + + const searchRequest1 = { params: { 1: '1' } }; + const requestHash1 = createRequestHash(searchRequest1.params); + const searchId1 = 'searchId1'; + + const searchRequest2 = { params: { 2: '2' } }; + const requestHash2 = createRequestHash(searchRequest2.params); + const searchId2 = 'searchId1'; + + const searchRequest3 = { params: { 3: '3' } }; + const requestHash3 = createRequestHash(searchRequest3.params); + const searchId3 = 'searchId3'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + await Promise.all([ + service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { + sessionId: sessionId2, + strategy: MOCK_STRATEGY, + }), + ]); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; + expect(type1).toBe(SEARCH_SESSION_TYPE); + expect(id1).toBe(sessionId1); + expect(callAttributes1).toHaveProperty('idMapping', { + [requestHash1]: { + id: searchId1, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + [requestHash2]: { + id: searchId2, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes1).toHaveProperty('touched'); + + const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; + expect(type2).toBe(SEARCH_SESSION_TYPE); + expect(id2).toBe(sessionId2); + expect(callAttributes2).toHaveProperty('idMapping', { + [requestHash3]: { + id: searchId3, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes2).toHaveProperty('touched'); }); - expect(callAttributes2).toHaveProperty('touched'); }); - }); - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: false, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Cannot get search ID from a session that is not stored]` - ); - }); + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: true, + isRestore: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + [requestHash]: { + id: searchId, + }, + }, + }, + }; + savedObjectsClient.get.mockResolvedValue(mockSession); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { sessionId, isStored: true, - isRestore: false, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Get search ID is only supported when restoring a session]` - ); - }); - - it('returns the search ID from the saved object ID mapping', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - [requestHash]: { - id: searchId, - }, - }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + isRestore: true, + }); - const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: true, - isRestore: true, + expect(id).toBe(searchId); }); - - expect(id).toBe(searchId); }); - }); - describe('getSearchIdMapping', () => { - it('retrieves the search IDs and strategies from the saved object', async () => { - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - foo: { - id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', - strategy: MOCK_STRATEGY, + describe('getSearchIdMapping', () => { + it('retrieves the search IDs and strategies from the saved object', async () => { + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + foo: { + id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', + strategy: MOCK_STRATEGY, + }, }, }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); - const searchIdMapping = await service.getSearchIdMapping( - { savedObjectsClient }, - mockUser1, - mockSession.id - ); - expect(searchIdMapping).toMatchInlineSnapshot(` - Map { - "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", - } - `); + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + const searchIdMapping = await service.getSearchIdMapping( + { savedObjectsClient }, + mockUser1, + mockSession.id + ); + expect(searchIdMapping).toMatchInlineSnapshot(` + Map { + "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", + } + `); + }); }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index c95c58a8dc06b..b5f7da594d53b 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -29,6 +29,7 @@ import { TaskManagerStartContract, } from '../../../../task_manager/server'; import { + ENHANCED_ES_SEARCH_STRATEGY, SearchSessionRequestInfo, SearchSessionSavedObjectAttributes, SearchSessionStatus, @@ -36,8 +37,13 @@ import { } from '../../../common'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; -import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; +import { + registerSearchSessionsTask, + scheduleSearchSessionsTasks, + unscheduleSearchSessionsTask, +} from './monitoring_task'; import { SearchSessionsConfig, SearchStatus } from './types'; +import { DataEnhancedStartDependencies } from '../../type'; export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -78,7 +84,7 @@ export class SearchSessionService this.sessionConfig = this.config.search.sessions; } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { config: this.config, taskManager: deps.taskManager, @@ -99,6 +105,8 @@ export class SearchSessionService this.logger, this.sessionConfig.trackingInterval ); + } else { + unscheduleSearchSessionsTask(deps.taskManager, this.logger); } }; @@ -217,6 +225,7 @@ export class SearchSessionService restoreState = {}, }: Partial ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); if (!name) throw new Error('Name is required'); if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); @@ -316,6 +325,7 @@ export class SearchSessionService attributes: Partial ) => { this.logger.debug(`update | ${sessionId}`); + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.update( SEARCH_SESSION_TYPE, @@ -353,6 +363,7 @@ export class SearchSessionService user: AuthenticatedUser | null, sessionId: string ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); this.logger.debug(`delete | ${sessionId}`); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); @@ -367,9 +378,9 @@ export class SearchSessionService user: AuthenticatedUser | null, searchRequest: IKibanaSearchRequest, searchId: string, - { sessionId, strategy }: ISearchOptions + { sessionId, strategy = ENHANCED_ES_SEARCH_STRATEGY }: ISearchOptions ) => { - if (!sessionId || !searchId) return; + if (!this.sessionConfig.enabled || !sessionId || !searchId) return; this.logger.debug(`trackId | ${sessionId} | ${searchId}`); let idMapping: Record = {}; @@ -378,7 +389,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); const searchInfo = { id: searchId, - strategy: strategy!, + strategy, status: SearchStatus.IN_PROGRESS, }; idMapping = { [requestHash]: searchInfo }; @@ -411,7 +422,9 @@ export class SearchSessionService searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions ) => { - if (!sessionId) { + if (!this.sessionConfig.enabled) { + throw new Error('Search sessions are disabled'); + } else if (!sessionId) { throw new Error('Session ID is required'); } else if (!isStored) { throw new Error('Cannot get search ID from a session that is not stored'); diff --git a/x-pack/plugins/data_enhanced/server/type.ts b/x-pack/plugins/data_enhanced/server/type.ts index c4a16eab1a3a7..215700c5dcc5c 100644 --- a/x-pack/plugins/data_enhanced/server/type.ts +++ b/x-pack/plugins/data_enhanced/server/type.ts @@ -7,6 +7,13 @@ import type { IRouter } from 'kibana/server'; import type { DataRequestHandlerContext } from '../../../../src/plugins/data/server'; +import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; /** * @internal @@ -17,3 +24,15 @@ export type DataEnhancedRequestHandlerContext = DataRequestHandlerContext; * @internal */ export type DataEnhancedPluginRouter = IRouter; + +export interface DataEnhancedSetupDependencies { + data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; + taskManager: TaskManagerSetupContract; + security?: SecurityPluginSetup; +} + +export interface DataEnhancedStartDependencies { + data: DataPluginStart; + taskManager: TaskManagerStartContract; +} diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index 5538a2db3e4cd..047b9b06516ba 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index 2a055bd0e0710..38a55e557909b 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json index 99aea16a9aaba..50fe41c49b0c8 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/embeddable_enhanced/tsconfig.json b/x-pack/plugins/embeddable_enhanced/tsconfig.json index 32754f2fd5524..6e9eb69585cbc 100644 --- a/x-pack/plugins/embeddable_enhanced/tsconfig.json +++ b/x-pack/plugins/embeddable_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 9eae8b7366bea..2b51b313d34fc 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index 133f704fd59a9..2325ddcf2b270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -19,6 +19,7 @@ export const mockKibanaValues = { history: mockHistory, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), + setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), renderHeaderActions: jest.fn(), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts new file mode 100644 index 0000000000000..6106cb049c7a1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mockApiLog = { + timestamp: '1970-01-01T12:00:00.000Z', + http_method: 'POST', + status: 200, + user_agent: 'Mozilla/5.0', + full_request_path: '/api/as/v1/engines/national-parks-demo/search.json', + request_body: '{"query":"test search"}', + response_body: + '{"meta":{"page":{"current":1,"total_pages":0,"total_results":0,"size":20}},"results":[]}', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx new file mode 100644 index 0000000000000..6bebeee80465c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout, EuiBadge } from '@elastic/eui'; + +import { ApiLogFlyout, ApiLogHeading } from './api_log_flyout'; + +describe('ApiLogFlyout', () => { + const values = { + isFlyoutOpen: true, + apiLog: mockApiLog, + }; + const actions = { + closeFlyout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('h2').text()).toEqual('Request details'); + expect(wrapper.find(ApiLogHeading).last().dive().find('h3').text()).toEqual('Response body'); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('POST'); + }); + + it('closes the flyout', () => { + const wrapper = shallow(); + + wrapper.find(EuiFlyout).simulate('close'); + expect(actions.closeFlyout).toHaveBeenCalled(); + }); + + it('does not render if the flyout is not open', () => { + setMockValues({ ...values, isFlyoutOpen: false }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('does not render if a current apiLog has not been set', () => { + setMockValues({ ...values, apiLog: null }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx new file mode 100644 index 0000000000000..dd53e997da0f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiPortal, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiHealth, + EuiText, + EuiCode, + EuiCodeBlock, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { getStatusColor, attemptToFormatJson } from '../utils'; + +import { ApiLogLogic } from './'; + +export const ApiLogFlyout: React.FC = () => { + const { isFlyoutOpen, apiLog } = useValues(ApiLogLogic); + const { closeFlyout } = useActions(ApiLogLogic); + + if (!isFlyoutOpen) return null; + if (!apiLog) return null; + + return ( + + + + +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.flyout.title', { + defaultMessage: 'Request details', + })} +

    +
    +
    + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTitle', { + defaultMessage: 'Method', + })} + +
    + {apiLog.http_method} +
    +
    + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTitle', { + defaultMessage: 'Status', + })} + + {apiLog.status} + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timestampTitle', { + defaultMessage: 'Timestamp', + })} + + {apiLog.timestamp} + +
    + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.userAgentTitle', { + defaultMessage: 'User agent', + })} + + + {apiLog.user_agent} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestPathTitle', { + defaultMessage: 'Request path', + })} + + + {apiLog.full_request_path} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestBodyTitle', { + defaultMessage: 'Request body', + })} + + + {attemptToFormatJson(apiLog.request_body)} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.responseBodyTitle', { + defaultMessage: 'Response body', + })} + + + {attemptToFormatJson(apiLog.response_body)} + +
    +
    +
    + ); +}; + +export const ApiLogHeading: React.FC = ({ children }) => ( + +

    {children}

    +
    +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx new file mode 100644 index 0000000000000..2b7ca7510e8e1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import { ApiLogLogic } from './'; + +describe('ApiLogLogic', () => { + const { mount } = new LogicMounter(ApiLogLogic); + + const DEFAULT_VALUES = { + isFlyoutOpen: false, + apiLog: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ApiLogLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('openFlyout', () => { + it('sets isFlyoutOpen to true & sets the current apiLog', () => { + mount({ isFlyoutOpen: false, apiLog: null }); + ApiLogLogic.actions.openFlyout(mockApiLog); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: true, + apiLog: mockApiLog, + }); + }); + }); + + describe('closeFlyout', () => { + it('sets isFlyoutOpen to false & resets the current apiLog', () => { + mount({ isFlyoutOpen: true, apiLog: mockApiLog }); + ApiLogLogic.actions.closeFlyout(); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: false, + apiLog: null, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts new file mode 100644 index 0000000000000..8b7c5f70f605c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { ApiLog } from '../types'; + +interface ApiLogValues { + isFlyoutOpen: boolean; + apiLog: ApiLog | null; +} + +interface ApiLogActions { + openFlyout(apiLog: ApiLog): { apiLog: ApiLog }; + closeFlyout(): void; +} + +export const ApiLogLogic = kea>({ + path: ['enterprise_search', 'app_search', 'api_log_logic'], + actions: () => ({ + openFlyout: (apiLog) => ({ apiLog }), + closeFlyout: true, + }), + reducers: () => ({ + isFlyoutOpen: [ + false, + { + openFlyout: () => true, + closeFlyout: () => false, + }, + ], + apiLog: [ + null, + { + openFlyout: (_, { apiLog }) => apiLog, + closeFlyout: () => null, + }, + ], + }), +}); diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts similarity index 72% rename from x-pack/plugins/security_solution/server/graphql/hosts/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts index 400405509b55a..dcf949d9bf222 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { createHostsResolvers } from './resolvers'; -export { hostsSchema } from './schema.gql'; +export { ApiLogFlyout } from './api_log_flyout'; +export { ApiLogLogic } from './api_log_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx index 8ca15906783f9..4690911fad772 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -26,6 +26,7 @@ import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; +import { ApiLogFlyout } from './api_log'; import { ApiLogsTable, NewApiEventsPrompt } from './components'; import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; @@ -75,6 +76,7 @@ export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts index 7b3ee80668ac7..2eda4c6323fa5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts @@ -6,6 +6,7 @@ */ import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { mockApiLog } from './__mocks__/api_log.mock'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; @@ -29,17 +30,7 @@ describe('ApiLogsLogic', () => { }; const MOCK_API_RESPONSE = { - results: [ - { - timestamp: '1970-01-01T12:00:00.000Z', - http_method: 'POST', - status: 200, - user_agent: 'some browser agent string', - full_request_path: '/api/as/v1/engines/national-parks-demo/search.json', - request_body: '{"someMockRequest":"hello"}', - response_body: '{"someMockResponse":"world"}', - }, - ], + results: [mockApiLog, mockApiLog], meta: { page: { current: 1, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx index 99fce81ca348f..768295ec1389c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -53,6 +53,7 @@ describe('ApiLogsTable', () => { }; const actions = { onPaginate: jest.fn(), + openFlyout: jest.fn(), }; beforeEach(() => { @@ -86,7 +87,7 @@ describe('ApiLogsTable', () => { expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3); wrapper.find('[data-test-subj="ApiLogsTableDetailsButton"]').first().simulate('click'); - // TODO: API log details flyout + expect(actions.openFlyout).toHaveBeenCalled(); }); it('renders an empty prompt if no items are passed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index 8ebcc4350f7fc..5ecf8e1ba3330 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -22,6 +22,7 @@ import { FormattedRelative } from '@kbn/i18n/react'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; +import { ApiLogLogic } from '../api_log'; import { ApiLogsLogic } from '../index'; import { ApiLog } from '../types'; import { getStatusColor } from '../utils'; @@ -34,6 +35,7 @@ interface Props { export const ApiLogsTable: React.FC = ({ hasPagination }) => { const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic); const { onPaginate } = useActions(ApiLogsLogic); + const { openFlyout } = useActions(ApiLogLogic); const columns: Array> = [ { @@ -81,7 +83,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => { size="s" className="apiLogDetailButton" data-test-subj="ApiLogsTableDetailsButton" - // TODO: flyout onclick + onClick={() => openFlyout(apiLog)} > {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.detailsButtonLabel', { defaultMessage: 'Details', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index 183956e51d8d4..568026dab231f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -7,5 +7,6 @@ export { API_LOGS_TITLE } from './constants'; export { ApiLogsTable, NewApiEventsPrompt } from './components'; +export { ApiLogFlyout } from './api_log'; export { ApiLogs } from './api_logs'; export { ApiLogsLogic } from './api_logs_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts index f9b6dcea2cbf3..ac464e2af353d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getDateString, getStatusColor } from './utils'; +import dedent from 'dedent'; + +import { getDateString, getStatusColor, attemptToFormatJson } from './utils'; describe('getDateString', () => { const mockDate = jest @@ -32,3 +34,20 @@ describe('getStatusColor', () => { expect(getStatusColor(503)).toEqual('danger'); }); }); + +describe('attemptToFormatJson', () => { + it('takes an unformatted JSON string and correctly newlines/indents it', () => { + expect(attemptToFormatJson('{"hello":"world","lorem":{"ipsum":"dolor","sit":"amet"}}')) + .toEqual(dedent`{ + "hello": "world", + "lorem": { + "ipsum": "dolor", + "sit": "amet" + } + }`); + }); + + it('returns the original content if it is not properly formatted JSON', () => { + expect(attemptToFormatJson('{invalid json}')).toEqual('{invalid json}'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts index 3217a1561ce76..7e5f19686f13b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts @@ -19,3 +19,13 @@ export const getStatusColor = (status: number) => { if (status >= 500) color = 'danger'; return color; }; + +export const attemptToFormatJson = (possibleJson: string) => { + try { + // it is JSON, we can format it with newlines/indentation + return JSON.stringify(JSON.parse(possibleJson), null, 2); + } catch { + // if it's not JSON, we return the original content + return possibleJson; + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index 3686f380407e2..18f27c3a1e834 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; -import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt } from '../../api_logs'; +import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt, ApiLogFlyout } from '../../api_logs'; import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { DataPanel } from '../../data_panel'; import { generateEnginePath } from '../../engine'; @@ -46,6 +46,7 @@ export const RecentApiLogs: React.FC = () => { hasBorder > + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index fb3b771850a31..df87f2e5230db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -26,7 +26,7 @@ export const EnginesOverviewHeader: React.FC = () => { rightSideItems={[ // eslint-disable-next-line @elastic/eui/href-or-on-click { {canManageEngines && ( @@ -108,6 +109,7 @@ export const EnginesOverview: React.FC = () => { + { return (
    +

    {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx index 5e5ee2ea8d0f0..911e97de5b53f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -48,7 +48,7 @@ export const RelevanceTuningPreview: React.FC = () => { const { engineName, isMetaEngine } = useValues(EngineLogic); return ( - +

    {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.title', { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts similarity index 72% rename from x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts index 650a0a9b82391..0bd18ea640850 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './location_map'; -export * from '../availability_reporting/location_status_tags'; +export { QueryPerformance } from './query_performance'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx new file mode 100644 index 0000000000000..0c62b783a47ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBadge } from '@elastic/eui'; + +import { QueryPerformance } from './query_performance'; + +describe('QueryPerformance', () => { + const values = { + queryPerformanceScore: 1, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders as green with the text "optimal" for a performance score of less than 6', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#59deb4'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: optimal'); + }); + + it('renders as blue with the text "good" for a performance score of less than 11', () => { + setMockValues({ + queryPerformanceScore: 10, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#40bfff'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: good'); + }); + + it('renders as yellow with the text "standard" for a performance score of less than 21', () => { + setMockValues({ + queryPerformanceScore: 20, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#fed566'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: standard'); + }); + + it('renders as red with the text "delayed" for a performance score of 21 or more', () => { + setMockValues({ + queryPerformanceScore: 100, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#ff9173'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: delayed'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx new file mode 100644 index 0000000000000..e3dfddc35d88c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +enum QueryPerformanceRating { + Optimal = 'Optimal', + Good = 'Good', + Standard = 'Standard', + Delayed = 'Delayed', +} + +const QUERY_PERFORMANCE_LABEL = (performanceValue: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformanceLabel', { + defaultMessage: 'Query performance: {performanceValue}', + values: { + performanceValue, + }, + }); + +const QUERY_PERFORMANCE_OPTIMAL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.optimalValue', + { defaultMessage: 'optimal' } +); + +const QUERY_PERFORMANCE_GOOD = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.goodValue', + { defaultMessage: 'good' } +); + +const QUERY_PERFORMANCE_STANDARD = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.standardValue', + { defaultMessage: 'standard' } +); + +const QUERY_PERFORMANCE_DELAYED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.delayedValue', + { defaultMessage: 'delayed' } +); + +const badgeText: Record = { + [QueryPerformanceRating.Optimal]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_OPTIMAL), + [QueryPerformanceRating.Good]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_GOOD), + [QueryPerformanceRating.Standard]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_STANDARD), + [QueryPerformanceRating.Delayed]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_DELAYED), +}; + +const badgeColors: Record = { + [QueryPerformanceRating.Optimal]: '#59deb4', + [QueryPerformanceRating.Good]: '#40bfff', + [QueryPerformanceRating.Standard]: '#fed566', + [QueryPerformanceRating.Delayed]: '#ff9173', +}; + +const getPerformanceRating = (score: number) => { + switch (true) { + case score < 6: + return QueryPerformanceRating.Optimal; + case score < 11: + return QueryPerformanceRating.Good; + case score < 21: + return QueryPerformanceRating.Standard; + default: + return QueryPerformanceRating.Delayed; + } +}; + +export const QueryPerformance: React.FC = () => { + const { queryPerformanceScore } = useValues(ResultSettingsLogic); + const performanceRating = getPerformanceRating(queryPerformanceScore); + return ( + + {badgeText[performanceRating]} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index 3388894c230a0..9eda1362e04fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -7,7 +7,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; -import { setMockActions } from '../../../__mocks__'; +import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; @@ -15,12 +15,19 @@ import { shallow } from 'enzyme'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; describe('RelevanceTuning', () => { + const values = { + dataLoading: false, + }; + const actions = { initializeResultSettingsData: jest.fn(), }; + beforeEach(() => { + setMockValues(values); setMockActions(actions); jest.clearAllMocks(); }); @@ -28,10 +35,20 @@ describe('RelevanceTuning', () => { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(true); + expect(wrapper.find(SampleResponse).exists()).toBe(true); }); it('initializes result settings data when mounted', () => { shallow(); expect(actions.initializeResultSettingsData).toHaveBeenCalled(); }); + + it('renders a loading screen if data has not loaded yet', () => { + setMockValues({ + dataLoading: true, + }); + const wrapper = shallow(); + expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); + expect(wrapper.find(SampleResponse).exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 38db5c60e98a9..336f3f663119f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -7,16 +7,20 @@ import React, { useEffect } from 'react'; -import { useActions } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiPageHeader, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; + import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; + import { ResultSettingsLogic } from '.'; interface Props { @@ -24,12 +28,15 @@ interface Props { } export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { + const { dataLoading } = useValues(ResultSettingsLogic); const { initializeResultSettingsData } = useActions(ResultSettingsLogic); useEffect(() => { initializeResultSettingsData(); }, []); + if (dataLoading) return ; + return ( <> @@ -40,7 +47,7 @@ export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { -
    TODO
    +
    diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index e7bb065b596c3..a9c161b2bb5be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -40,6 +40,7 @@ describe('ResultSettingsLogic', () => { stagedUpdates: false, nonTextResultFields: {}, textResultFields: {}, + queryPerformanceScore: 0, }; // Values without selectors @@ -487,6 +488,76 @@ describe('ResultSettingsLogic', () => { }); }); }); + + describe('queryPerformanceScore', () => { + describe('returns a score for the current query performance based on the result settings', () => { + it('considers a text value with raw set (but no size) as worth 1.5', () => { + mount({ + resultFields: { foo: { raw: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); + }); + + it('considers a text value with raw set and a size over 250 as also worth 1.5', () => { + mount({ + resultFields: { foo: { raw: true, rawSize: 251 } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); + }); + + it('considers a text value with raw set and a size less than or equal to 250 as worth 1', () => { + mount({ + resultFields: { foo: { raw: true, rawSize: 250 } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1); + }); + + it('considers a text value with a snippet set as worth 2', () => { + mount({ + resultFields: { foo: { snippet: true, snippetSize: 50, snippetFallback: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(2); + }); + + it('will sum raw and snippet values if both are set', () => { + mount({ + resultFields: { foo: { snippet: true, raw: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + // 1.5 (raw) + 2 (snippet) = 3.5 + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(3.5); + }); + + it('considers a non-text value with raw set as 0.2', () => { + mount({ + resultFields: { foo: { raw: true } }, + schema: { foo: 'number' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(0.2); + }); + + it('can sum variations of all the prior', () => { + mount({ + resultFields: { + foo: { raw: true }, + bar: { raw: true, snippet: true }, + baz: { raw: true }, + }, + schema: { + foo: 'text' as SchemaTypes, + bar: 'text' as SchemaTypes, + baz: 'number' as SchemaTypes, + }, + }); + // 1.5 (foo) + 3.5 (bar) + baz (.2) = 5.2 + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(5.2); + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index 22f4c44f8b543..c345ae7e02e8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -71,18 +71,19 @@ interface ResultSettingsValues { dataLoading: boolean; saving: boolean; openModal: OpenModal; - nonTextResultFields: FieldResultSettingObject; - textResultFields: FieldResultSettingObject; resultFields: FieldResultSettingObject; - serverResultFields: ServerFieldResultSettingObject; lastSavedResultFields: FieldResultSettingObject; schema: Schema; schemaConflicts: SchemaConflicts; // Selectors + textResultFields: FieldResultSettingObject; + nonTextResultFields: FieldResultSettingObject; + serverResultFields: ServerFieldResultSettingObject; resultFieldsAtDefaultSettings: boolean; resultFieldsEmpty: boolean; stagedUpdates: true; reducedServerResultFields: ServerFieldResultSettingObject; + queryPerformanceScore: number; } export const ResultSettingsLogic = kea>({ @@ -221,6 +222,31 @@ export const ResultSettingsLogic = kea [selectors.serverResultFields, selectors.schema], + (serverResultFields: ServerFieldResultSettingObject, schema: Schema) => { + return Object.entries(serverResultFields).reduce((acc, [fieldName, resultField]) => { + let newAcc = acc; + if (resultField.raw) { + if (schema[fieldName] !== 'text') { + newAcc += 0.2; + } else if ( + typeof resultField.raw === 'object' && + resultField.raw.size && + resultField.raw.size <= 250 + ) { + newAcc += 1.0; + } else { + newAcc += 1.5; + } + } + if (resultField.snippet) { + newAcc += 2.0; + } + return newAcc; + }, 0); + }, + ], }), listeners: ({ actions, values }) => ({ clearRawSizeForField: ({ fieldName }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx index 145654be20461..dc91b5039a3c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { useValues, useActions } from 'kea'; import { EuiTableRow, EuiTableRowCell, EuiCheckbox, EuiTableRowCellCheckbox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ResultSettingsLogic } from '..'; import { FieldResultSetting } from '../types'; @@ -33,6 +34,10 @@ export const NonTextFieldsBody: React.FC = () => { { { { { + const actions = { + queryChanged: jest.fn(), + getSearchResults: jest.fn(), + }; + + const values = { + reducedServerResultFields: {}, + query: 'foo', + response: { + bar: 'baz', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + setMockValues(values); + }); + + it('renders a text box with the current user "query" value from state', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('updates the "query" value in state when a user updates the text in the text box', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } }); + expect(actions.queryChanged).toHaveBeenCalledWith('bar'); + }); + + it('will call getSearchResults with the current value of query and reducedServerResultFields in a useEffect, which updates the displayed response', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('renders the response from the given user "query" in a code block', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('{\n "bar": "baz"\n}'); + }); + + it('renders a plain old string in the code block if the response is a string', () => { + setMockValues({ + response: 'No results.', + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('No results.'); + }); + + it('will not render a code block at all if there is no response yet', () => { + setMockValues({ + response: null, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx new file mode 100644 index 0000000000000..2d0cced3730ba --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiCodeBlock, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { QueryPerformance } from '../query_performance'; +import { ResultSettingsLogic } from '../result_settings_logic'; + +import { SampleResponseLogic } from './sample_response_logic'; + +export const SampleResponse: React.FC = () => { + const { reducedServerResultFields } = useValues(ResultSettingsLogic); + + const { query, response } = useValues(SampleResponseLogic); + const { queryChanged, getSearchResults } = useActions(SampleResponseLogic); + + useEffect(() => { + getSearchResults(query, reducedServerResultFields); + }, [query, reducedServerResultFields]); + + return ( + + + + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponseTitle', + { defaultMessage: 'Sample response' } + )} +

    +
    +
    + + + +
    + + queryChanged(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.inputPlaceholder', + { defaultMessage: 'Type a search query to test a response...' } + )} + data-test-subj="ResultSettingsQuerySampleResponse" + /> + + {!!response && ( + + {typeof response === 'string' ? response : JSON.stringify(response, null, 2)} + + )} +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts new file mode 100644 index 0000000000000..79379306c1618 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, mockHttpValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { SampleResponseLogic } from './sample_response_logic'; + +describe('SampleResponseLogic', () => { + const { mount } = new LogicMounter(SampleResponseLogic); + const { http } = mockHttpValues; + + const DEFAULT_VALUES = { + query: '', + response: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + }); + }); + + describe('actions', () => { + describe('queryChanged', () => { + it('updates the query', () => { + mount({ + query: '', + }); + + SampleResponseLogic.actions.queryChanged('foo'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + query: 'foo', + }); + }); + }); + + describe('getSearchResultsSuccess', () => { + it('sets the response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsSuccess({}); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: {}, + }); + }); + }); + + describe('getSearchResultsFailure', () => { + it('sets a string response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsFailure('An error occured.'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: 'An error occured.', + }); + }); + }); + }); + + describe('listeners', () => { + describe('getSearchResults', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('makes a search API request and calls getSearchResultsSuccess with the first result of the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [ + { id: { raw: 'foo' }, _meta: {} }, + { id: { raw: 'bar' }, _meta: {} }, + { id: { raw: 'baz' }, _meta: {} }, + ], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith({ + // Note that the _meta field was stripped from the result + id: { raw: 'foo' }, + }); + }); + + it('calls getSearchResultsSuccess with a "No Results." message if there are no results', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith( + 'No results.' + ); + }); + + it('handles 500 errors by setting a generic error response and showing a flash message error', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + const error = { + response: { + status: 500, + }, + }; + + http.post.mockReturnValueOnce(Promise.reject(error)); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('handles 400 errors by setting the response, but does not show a flash error message', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + body: { + attributes: { + errors: ['A validation error occurred.'], + }, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith({ + errors: ['A validation error occurred.'], + }); + }); + + it('sets a generic message on a 400 error if no custom message is provided in the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('does nothing if an empty object is passed for the resultFields parameter', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + SampleResponseLogic.actions.getSearchResults('foo', {}); + + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts new file mode 100644 index 0000000000000..808a7ec9c65dc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { HttpLogic } from '../../../../shared/http'; +import { EngineLogic } from '../../engine'; + +import { SampleSearchResponse, ServerFieldResultSettingObject } from '../types'; + +const NO_RESULTS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.noResultsMessage', + { defaultMessage: 'No results.' } +); + +const ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.errorMessage', + { defaultMessage: 'An error occured.' } +); + +interface SampleResponseValues { + query: string; + response: SampleSearchResponse | string | null; +} + +interface SampleResponseActions { + queryChanged: (query: string) => { query: string }; + getSearchResultsSuccess: ( + response: SampleSearchResponse | string + ) => { response: SampleSearchResponse | string }; + getSearchResultsFailure: (response: string) => { response: string }; + getSearchResults: ( + query: string, + resultFields: ServerFieldResultSettingObject + ) => { query: string; resultFields: ServerFieldResultSettingObject }; +} + +export const SampleResponseLogic = kea>({ + path: ['enterprise_search', 'app_search', 'sample_response_logic'], + actions: { + queryChanged: (query) => ({ query }), + getSearchResultsSuccess: (response) => ({ response }), + getSearchResultsFailure: (response) => ({ response }), + getSearchResults: (query, resultFields) => ({ query, resultFields }), + }, + reducers: { + query: ['', { queryChanged: (_, { query }) => query }], + response: [ + null, + { + getSearchResultsSuccess: (_, { response }) => response, + getSearchResultsFailure: (_, { response }) => response, + }, + ], + }, + listeners: ({ actions }) => ({ + getSearchResults: async ({ query, resultFields }, breakpoint) => { + if (Object.keys(resultFields).length < 1) return; + await breakpoint(250); + + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/sample_response_search`; + + try { + const response = await http.post(url, { + body: JSON.stringify({ + query, + result_fields: resultFields, + }), + }); + + const result = response.results?.[0]; + actions.getSearchResultsSuccess( + result ? { ...result, _meta: undefined } : NO_RESULTS_MESSAGE + ); + } catch (e) { + if (e.response.status >= 500) { + // 4XX Validation errors are expected, as a user could enter something like 2 as a size, which is out of valid range. + // In this case, we simply render the message from the server as the response. + // + // 5xx Server errors are unexpected, and need to be reported in a flash message. + flashAPIErrors(e); + actions.getSearchResultsFailure(ERROR_MESSAGE); + } else { + actions.getSearchResultsFailure(e.body?.attributes || ERROR_MESSAGE); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 96bf277314a7b..18843112f46bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FieldValue } from '../result/types'; + export enum OpenModal { None, ConfirmResetModal, @@ -35,3 +37,5 @@ export interface FieldResultSetting { } export type FieldResultSettingObject = Record; + +export type SampleSearchResponse = Record; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 155ff5b92ba27..c2bf77751528a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -49,6 +49,7 @@ export const renderApp = ( history: params.history, navigateToUrl: core.application.navigateToUrl, setBreadcrumbs: core.chrome.setBreadcrumbs, + setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, renderHeaderActions: (HeaderActions) => params.setHeaderActionMenu((el) => renderHeaderActions(HeaderActions, store, el)), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 8015d22f7c44a..2bef7d373f160 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -24,6 +24,7 @@ interface KibanaLogicProps { charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; } @@ -47,6 +48,7 @@ export const KibanaLogic = kea>({ {}, ], setBreadcrumbs: [props.setBreadcrumbs, {}], + setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], renderHeaderActions: [props.renderHeaderActions, {}], }), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index 0bf7d618c33b3..c05c4dcbdddc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -14,6 +14,7 @@ jest.mock('../react_router_helpers', () => ({ import { letBrowserHandleEvent } from '../react_router_helpers'; import { + Breadcrumb, useGenerateBreadcrumbs, useEuiBreadcrumbs, useEnterpriseSearchBreadcrumbs, @@ -40,6 +41,9 @@ describe('useGenerateBreadcrumbs', () => { { text: 'Groups', path: '/groups' }, { text: 'Example Group Name', path: '/groups/{id}' }, { text: 'Source Prioritization', path: '/groups/{id}/source_prioritization' }, + // Note: We're still generating a path for the last breadcrumb even though useEuiBreadcrumbs + // will not render a link for it. This is because it's easier to keep our last-breadcrumb-specific + // logic in one place, & this way we still have a current path if (for some reason) we need it later. ]); }); @@ -89,48 +93,51 @@ describe('useEuiBreadcrumbs', () => { }, { text: 'World', - href: '/app/enterprise_search/world', - onClick: expect.any(Function), + // Per EUI best practices, the last breadcrumb is inactive/is not a link }, ]); }); - it('prevents default navigation and uses React Router history on click', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/test' }])[0] as any; + describe('link behavior for non-last breadcrumbs', () => { + // Test helper - adds a 2nd dummy breadcrumb so that paths from the first breadcrumb are generated + const useEuiBreadcrumb = (breadcrumb: Breadcrumb) => + useEuiBreadcrumbs([breadcrumb, { text: '' }])[0] as any; - expect(breadcrumb.href).toEqual('/app/enterprise_search/test'); - expect(mockHistory.createHref).toHaveBeenCalled(); + it('prevents default navigation and uses React Router history on click', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test' }); - const event = { preventDefault: jest.fn() }; - breadcrumb.onClick(event); + expect(breadcrumb.href).toEqual('/app/enterprise_search/test'); + expect(mockHistory.createHref).toHaveBeenCalled(); - expect(event.preventDefault).toHaveBeenCalled(); - expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); - }); + const event = { preventDefault: jest.fn() }; + breadcrumb.onClick(event); - it('does not call createHref if shouldNotCreateHref is passed', () => { - const breadcrumb = useEuiBreadcrumbs([ - { text: '', path: '/test', shouldNotCreateHref: true }, - ])[0] as any; + expect(event.preventDefault).toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); + }); - expect(breadcrumb.href).toEqual('/test'); - expect(mockHistory.createHref).not.toHaveBeenCalled(); - }); + it('does not call createHref if shouldNotCreateHref is passed', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test', shouldNotCreateHref: true }); - it('does not prevent default browser behavior on new tab/window clicks', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/' }])[0] as any; + expect(breadcrumb.href).toEqual('/test'); + expect(mockHistory.createHref).not.toHaveBeenCalled(); + }); - (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); - breadcrumb.onClick(); + it('does not prevent default browser behavior on new tab/window clicks', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/' }); - expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); - }); + (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); + breadcrumb.onClick(); + + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); + }); - it('does not generate link behavior if path is excluded', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: 'Unclickable breadcrumb' }])[0]; + it('does not generate link behavior if path is excluded', () => { + const breadcrumb = useEuiBreadcrumb({ text: 'Unclickable breadcrumb' }); - expect(breadcrumb.href).toBeUndefined(); - expect(breadcrumb.onClick).toBeUndefined(); + expect(breadcrumb.href).toBeUndefined(); + expect(breadcrumb.onClick).toBeUndefined(); + }); }); }); @@ -164,8 +171,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -174,8 +179,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => { expect(useEnterpriseSearchBreadcrumbs()).toEqual([ { text: 'Enterprise Search', - href: '/app/enterprise_search/overview', - onClick: expect.any(Function), }, ]); }); @@ -219,8 +222,6 @@ describe('useAppSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/app_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -234,8 +235,6 @@ describe('useAppSearchBreadcrumbs', () => { }, { text: 'App Search', - href: '/app/enterprise_search/app_search/', - onClick: expect.any(Function), }, ]); }); @@ -279,8 +278,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/workplace_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -294,8 +291,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => { }, { text: 'Workplace Search', - href: '/app/enterprise_search/workplace_search/', - onClick: expect.any(Function), }, ]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 908cc0601ab9c..5855dc6990f6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -24,7 +24,7 @@ import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; * Types */ -interface Breadcrumb { +export interface Breadcrumb { text: string; path?: string; // Used to navigate outside of the React Router basename, @@ -64,16 +64,20 @@ export const useGenerateBreadcrumbs = (trail: BreadcrumbTrail): Breadcrumbs => { /** * Convert IBreadcrumb objects to React-Router-friendly EUI breadcrumb objects * https://elastic.github.io/eui/#/navigation/breadcrumbs + * + * NOTE: Per EUI best practices, we remove the link behavior and + * generate an inactive breadcrumb for the last breadcrumb in the list. */ export const useEuiBreadcrumbs = (breadcrumbs: Breadcrumbs): EuiBreadcrumb[] => { const { navigateToUrl, history } = useValues(KibanaLogic); const { http } = useValues(HttpLogic); - return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => { + return breadcrumbs.map(({ text, path, shouldNotCreateHref }, i) => { const breadcrumb: EuiBreadcrumb = { text }; + const isLastBreadcrumb = i === breadcrumbs.length - 1; - if (path) { + if (path && !isLastBreadcrumb) { breadcrumb.href = createHref(path, { history, http }, { shouldNotCreateHref }); breadcrumb.onClick = (event) => { if (letBrowserHandleEvent(event)) return; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx new file mode 100644 index 0000000000000..e8035f01a9405 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiHeader, EuiPopover } from '@elastic/eui'; + +import { AccountHeader } from './'; + +describe('AccountHeader', () => { + const mockValues = { + account: { + isAdmin: true, + }, + }; + + beforeEach(() => { + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiHeader)).toHaveLength(1); + }); + + describe('accountSubNav', () => { + it('handles popover trigger click', () => { + const wrapper = shallow(); + const popover = wrapper.find(EuiPopover); + const onClick = popover.dive().find('[data-test-subj="AccountButton"]').prop('onClick'); + onClick!({} as any); + + expect(onClick).toBeDefined(); + }); + + it('handles close popover', () => { + const wrapper = shallow(); + const popover = wrapper.find(EuiPopover); + popover.prop('closePopover')!(); + + expect(popover.prop('isOpen')).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx new file mode 100644 index 0000000000000..a878d87af09e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiHeader, + EuiHeaderLogo, + EuiHeaderLinks, + EuiHeaderSection, + EuiHeaderSectionItem, + EuiText, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, +} from '@elastic/eui'; + +import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import { WORKPLACE_SEARCH_TITLE, ACCOUNT_NAV } from '../../../constants'; +import { + ALPHA_PATH, + PERSONAL_SOURCES_PATH, + LOGOUT_ROUTE, + KIBANA_ACCOUNT_ROUTE, +} from '../../../routes'; + +export const AccountHeader: React.FC = () => { + const [isPopoverOpen, setPopover] = useState(false); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + const closePopover = () => { + setPopover(false); + }; + + const { + account: { isAdmin }, + } = useValues(AppLogic); + + const accountNavItems = [ + + {/* TODO: Once auth is completed, we need to have non-admins redirect to the self-hosted form */} + {ACCOUNT_NAV.SETTINGS} + , + + {ACCOUNT_NAV.LOGOUT} + , + ]; + + const accountButton = ( + + {ACCOUNT_NAV.ACCOUNT} + + ); + + return ( + + + + + {WORKPLACE_SEARCH_TITLE} + + + + {ACCOUNT_NAV.SOURCES} + + + + + + {isAdmin && ( + {ACCOUNT_NAV.ORG_DASHBOARD} + )} + + + + + {ACCOUNT_NAV.SEARCH} + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts similarity index 83% rename from x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts index 22d195c6e4253..e6cd2516fc03a 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KueryBar } from './kuery_bar'; +export { AccountHeader } from './account_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index 2678b5d01b475..b9a49c416f283 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -7,3 +7,4 @@ export { WorkplaceSearchNav } from './nav'; export { WorkplaceSearchHeaderActions } from './kibana_header_actions'; +export { AccountHeader } from './account_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index a6e9ce282bf3d..d771673506761 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -9,6 +9,13 @@ import { i18n } from '@kbn/i18n'; import { UPDATE_BUTTON_LABEL, SAVE_BUTTON_LABEL, CANCEL_BUTTON_LABEL } from '../shared/constants'; +export const WORKPLACE_SEARCH_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.title', + { + defaultMessage: 'Workplace Search', + } +); + export const NAV = { OVERVIEW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { defaultMessage: 'Overview', @@ -76,6 +83,30 @@ export const NAV = { }), }; +export const ACCOUNT_NAV = { + SOURCES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.sources.link', { + defaultMessage: 'Content sources', + }), + ORG_DASHBOARD: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link', + { + defaultMessage: 'Go to organizational dashboard', + } + ), + SEARCH: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.search.link', { + defaultMessage: 'Search', + }), + ACCOUNT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.account.link', { + defaultMessage: 'My account', + }), + SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.settings.link', { + defaultMessage: 'Account settings', + }), + LOGOUT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link', { + defaultMessage: 'Logout', + }), +}; + export const MAX_TABLE_ROW_ICONS = 3; export const SOURCE_STATUSES = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 48bdcd6551b65..a2c0ec18def4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -57,11 +57,13 @@ describe('WorkplaceSearchConfigured', () => { setMockActions({ initializeAppData, setContext }); }); - it('renders layout and header actions', () => { + it('renders layout, chrome, and header actions', () => { const wrapper = shallow(); expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(OverviewMVP)).toHaveLength(1); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c269a987dc092..a8d6fc54f7924 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -53,7 +53,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); - const { renderHeaderActions } = useValues(KibanaLogic); + const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); const { pathname } = useLocation(); @@ -66,12 +66,16 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - const isOrganization = !pathname.match(personalSourceUrlRegex); setContext(isOrganization); + useEffect(() => { + setChromeIsVisible(isOrganization); + }, [pathname]); + useEffect(() => { if (!hasInitialized) { initializeAppData(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 50f6596a860c5..e08050335671e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -12,6 +12,8 @@ import { docLinks } from '../shared/doc_links'; export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; +export const LOGOUT_ROUTE = '/logout'; +export const KIBANA_ACCOUNT_ROUTE = '/security/account'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; @@ -73,11 +75,11 @@ export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`; export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`; export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira_server`; -export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/onedrive`; +export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/one_drive`; export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce_sandbox`; export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`; -export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/sharepoint`; +export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/share_point`; export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; @@ -108,11 +110,11 @@ export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google_drive/edit`; export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_cloud/edit`; export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_server/edit`; -export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/onedrive/edit`; +export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one_drive/edit`; export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce_sandbox/edit`; export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`; -export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/sharepoint/edit`; +export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share_point/edit`; export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 99cebd5ded585..bf0c5471f7b57 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -33,7 +33,7 @@ export const SourceSubNav: React.FC = () => { const isCustom = serviceType === CUSTOM_SERVICE_TYPE; return ( - <> +
    {NAV.OVERVIEW} @@ -53,6 +53,6 @@ export const SourceSubNav: React.FC = () => { {NAV.SETTINGS} - +
    ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 488eb4b49853b..7558eb1e4e662 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -15,8 +15,11 @@ import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; +import { AccountHeader } from '../../components/layout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, @@ -40,6 +43,8 @@ describe('PrivateSourcesLayout', () => { const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + expect(wrapper.find(SourceSubNav)).toHaveLength(1); + expect(wrapper.find(AccountHeader)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index bdc2421432c8a..c565ee5f39a71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -12,8 +12,11 @@ import { useValues } from 'kea'; import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; +import { AccountHeader } from '../../components/layout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING, PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -46,21 +49,25 @@ export const PrivateSourcesLayout: React.FC = ({ : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; return ( - - - - - - {readOnlyMode && ( - - )} - {children} - - + <> + + + + + + + + {readOnlyMode && ( + + )} + {children} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index d20d0576d11ce..a9712cc4e1dc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -214,7 +214,7 @@ describe('SourceLogic', () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); expect(http.get).toHaveBeenCalledWith( - '/api/workplace_search/org/sources/123/federated_summary' + '/api/workplace_search/account/sources/123/federated_summary' ); await promise; expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 72700ce42c75d..3da90c4fc7739 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -156,7 +156,7 @@ export const SourceLogic = kea>({ } }, initializeFederatedSummary: async ({ sourceId }) => { - const route = `/api/workplace_search/org/sources/${sourceId}/federated_summary`; + const route = `/api/workplace_search/account/sources/${sourceId}/federated_summary`; try { const response = await HttpLogic.values.http.get(route); actions.onUpdateSummary(response.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index f142567fb621f..549ca3ae9154e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -20,13 +20,23 @@ .privateSourcesLayout { $sideBarWidth: $euiSize * 30; + $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes + $pageHeight: calc(100vh - #{$consoleHeaderHeight}); left: $sideBarWidth; width: calc(100% - #{$sideBarWidth}); + min-height: $pageHeight; &__sideBar { padding: 32px 40px 40px; width: $sideBarWidth; margin-left: -$sideBarWidth; + height: $pageHeight; + } +} + +.sourcesSubNav { + li { + display: block; } } diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index f00e81a5accf7..dd1a62d243d03 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -114,6 +114,9 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + // The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally + // here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes. + chrome.setIsVisible(false); await this.getInitialData(http); const pluginData = this.getPluginData(); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts index 8d1a7e3ead37b..e38380d60c6e9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts @@ -88,4 +88,48 @@ describe('result settings routes', () => { }); }); }); + + describe('POST /api/app_search/engines/{name}/sample_response_search', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/sample_response_search', + }); + + beforeEach(() => { + registerResultSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { + query: 'test', + result_fields: resultFields, + }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/sample_response_search', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + query: 'test', + result_fields: resultFields, + }, + }; + mockRouter.shouldValidate(request); + }); + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts index 38cb4aa922738..b091ae7a539c2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts @@ -45,4 +45,22 @@ export function registerResultSettingsRoutes({ path: '/as/engines/:engineName/result_settings', }) ); + + router.post( + { + path: '/api/app_search/engines/{engineName}/sample_response_search', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + query: schema.string(), + result_fields: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/sample_response_search', + }) + ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 8257dd0dc52b0..1dd6d859d88ad 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -22,9 +22,9 @@ const schemaValuesSchema = schema.recordOf( ); const pageSchema = schema.object({ - current: schema.number(), - size: schema.number(), - total_pages: schema.number(), + current: schema.nullable(schema.number()), + size: schema.nullable(schema.number()), + total_pages: schema.nullable(schema.number()), total_results: schema.number(), }); diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index a4f1c55463e75..6b4c50770b49f 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/event_log/tsconfig.json b/x-pack/plugins/event_log/tsconfig.json index e21dbc93b7b47..9b7cde10da3d6 100644 --- a/x-pack/plugins/event_log/tsconfig.json +++ b/x-pack/plugins/event_log/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/features/tsconfig.json b/x-pack/plugins/features/tsconfig.json index 11e2dbc8f093f..1260af55fbff6 100644 --- a/x-pack/plugins/features/tsconfig.json +++ b/x-pack/plugins/features/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index 0fc59e2b525a8..11cf4ac3615bf 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../src/plugins/data/common'; export interface HasImportPermission { @@ -83,7 +84,9 @@ export interface ImportResponse { pipelineId?: string; docCount: number; failures: ImportFailure[]; - error?: any; + error?: { + error: estypes.ErrorCause; + }; ingestError?: boolean; } diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index 29aed0cd52f7e..a3bc2ed082b1a 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -7,19 +7,20 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonIcon, EuiCallOut, EuiCopy, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { CodeEditor, KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { getHttp, getUiSettings } from '../kibana_services'; +import { getDocLinks, getHttp, getUiSettings } from '../kibana_services'; import { ImportResults } from '../importer'; const services = { @@ -27,8 +28,10 @@ const services = { }; interface Props { + failedPermissionCheck: boolean; importResults?: ImportResults; indexPatternResp?: object; + indexName: string; } export class ImportCompleteView extends Component { @@ -57,9 +60,12 @@ export class ImportCompleteView extends Component { iconType="copy" color="text" data-test-subj={copyButtonDataTestSubj} - aria-label={i18n.translate('xpack.fileUpload.copyButtonAriaLabel', { - defaultMessage: 'Copy to clipboard', - })} + aria-label={i18n.translate( + 'xpack.fileUpload.importComplete.copyButtonAriaLabel', + { + defaultMessage: 'Copy to clipboard', + } + )} /> )} @@ -90,21 +96,65 @@ export class ImportCompleteView extends Component { } _getStatusMsg() { + if (this.props.failedPermissionCheck) { + return ( + +

    + {i18n.translate('xpack.fileUpload.importComplete.permissionFailureMsg', { + defaultMessage: + 'You do not have permission to create or import data into index "{indexName}".', + values: { indexName: this.props.indexName }, + })} +

    + + {i18n.translate('xpack.fileUpload.importComplete.permission.docLink', { + defaultMessage: 'View file import permissions', + })} + +
    + ); + } + if (!this.props.importResults || !this.props.importResults.success) { - return i18n.translate('xpack.fileUpload.uploadFailureMsg', { - defaultMessage: 'File upload failed.', - }); + const errorMsg = + this.props.importResults && this.props.importResults.error + ? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', { + defaultMessage: 'Error: {reason}', + values: { reason: this.props.importResults.error.error.reason }, + }) + : ''; + return ( + +

    {errorMsg}

    +
    + ); } - const successMsg = i18n.translate('xpack.fileUpload.uploadSuccessMsg', { - defaultMessage: 'File upload complete: indexed {numFeatures} features.', + const successMsg = i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', { + defaultMessage: 'Indexed {numFeatures} features.', values: { numFeatures: this.props.importResults.docCount, }, }); const failedFeaturesMsg = this.props.importResults.failures?.length - ? i18n.translate('xpack.fileUpload.failedFeaturesMsg', { + ? i18n.translate('xpack.fileUpload.importComplete.failedFeaturesMsg', { defaultMessage: 'Unable to index {numFailures} features.', values: { numFailures: this.props.importResults.failures.length, @@ -112,47 +162,60 @@ export class ImportCompleteView extends Component { }) : ''; - return `${successMsg} ${failedFeaturesMsg}`; + return ( + +

    {`${successMsg} ${failedFeaturesMsg}`}

    +
    + ); + } + + _renderIndexManagementMsg() { + return this.props.importResults && this.props.importResults.success ? ( + +

    + + + + +

    +
    + ) : null; } render() { return ( - -

    {this._getStatusMsg()}

    -
    + {this._getStatusMsg()} + {this._renderCodeEditor( this.props.importResults, - i18n.translate('xpack.fileUpload.jsonImport.indexingResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexingResponse', { defaultMessage: 'Import response', }), 'indexRespCopyButton' )} {this._renderCodeEditor( this.props.indexPatternResp, - i18n.translate('xpack.fileUpload.jsonImport.indexPatternResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexPatternResponse', { defaultMessage: 'Index pattern response', }), 'indexPatternRespCopyButton' )} - -
    - - - - -
    -
    + {this._renderIndexManagementMsg()}
    ); } diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx index 371d68443bc2c..d73c6e9c5fb3a 100644 --- a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx +++ b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx @@ -16,6 +16,7 @@ import { FileUploadComponentProps } from '../lazy_load_bundle'; import { ImportResults } from '../importer'; import { GeoJsonImporter } from '../importer/geojson_importer'; import { Settings } from '../../common'; +import { hasImportPermission } from '../api'; enum PHASE { CONFIGURE = 'CONFIGURE', @@ -31,6 +32,7 @@ function getWritingToIndexMsg(progress: number) { } interface State { + failedPermissionCheck: boolean; geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE; importStatus: string; importResults?: ImportResults; @@ -45,6 +47,7 @@ export class JsonUploadAndParse extends Component ); } diff --git a/x-pack/plugins/file_upload/public/kibana_services.ts b/x-pack/plugins/file_upload/public/kibana_services.ts index a604136ca34e4..dfe2785e7a2bc 100644 --- a/x-pack/plugins/file_upload/public/kibana_services.ts +++ b/x-pack/plugins/file_upload/public/kibana_services.ts @@ -15,6 +15,7 @@ export function setStartServices(core: CoreStart, plugins: FileUploadStartDepend pluginsStart = plugins; } +export const getDocLinks = () => coreStart.docLinks; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getHttp = () => coreStart.http; export const getSavedObjectsClient = () => coreStart.savedObjects.client; diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index 8a982f83632aa..887a05af31174 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index a4cca4455a274..65b853ed5b38f 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -31,6 +31,7 @@ describe('Fleet - packageToPackagePolicy', () => { map: [], lens: [], ml_module: [], + security_rule: [], }, elasticsearch: { ingest_pipeline: [], diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 80fabd51613ae..3bc0d97d64646 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -50,6 +50,7 @@ export enum KibanaAssetType { indexPattern = 'index_pattern', map = 'map', lens = 'lens', + securityRule = 'security_rule', mlModule = 'ml_module', } @@ -64,6 +65,7 @@ export enum KibanaSavedObjectType { map = 'map', lens = 'lens', mlModule = 'ml-module', + securityRule = 'security-rule', } export enum ElasticsearchAssetType { diff --git a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts index b16234d5a5f97..c9fff1c1581bd 100644 --- a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts @@ -27,3 +27,7 @@ export interface PreconfiguredAgentPolicy extends Omit; } + +export interface PreconfiguredPackage extends Omit { + force?: boolean; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index 9ee0b0a7b29ee..bdf49f44f4397 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -48,6 +48,48 @@ export const AgentPolicyActionMenu = memo<{ return ( {(copyAgentPolicyPrompt) => { + const viewPolicyItem = ( + setIsYamlFlyoutOpen(!isYamlFlyoutOpen)} + key="viewPolicy" + > + + + ); + + const menuItems = agentPolicy?.is_managed + ? [viewPolicyItem] + : [ + setIsEnrollmentFlyoutOpen(true)} + key="enrollAgents" + > + + , + viewPolicyItem, + { + copyAgentPolicyPrompt(agentPolicy, onCopySuccess); + }} + key="copyPolicy" + > + + , + ]; return ( <> {isYamlFlyoutOpen ? ( @@ -80,42 +122,7 @@ export const AgentPolicyActionMenu = memo<{ } : undefined } - items={[ - setIsEnrollmentFlyoutOpen(true)} - key="enrollAgents" - > - - , - setIsYamlFlyoutOpen(!isYamlFlyoutOpen)} - key="viewPolicy" - > - - , - { - copyAgentPolicyPrompt(agentPolicy, onCopySuccess); - }} - key="copyPolicy" - > - - , - ]} + items={menuItems} /> ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index c859d585f4d82..de27d5fada755 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -148,12 +148,21 @@ export const AgentBulkActions: React.FunctionComponent<{ }, ]; + const showSelectEverything = + selectionMode === 'manual' && + selectedAgents.length === selectableAgents && + selectableAgents < totalAgents; + + const totalActiveAgents = totalAgents - totalInactiveAgents; + const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents; + const agents = selectionMode === 'manual' ? selectedAgents : currentQuery; + return ( <> {isReassignFlyoutOpen && ( { setIsReassignFlyoutOpen(false); refreshAgents(); @@ -164,10 +173,8 @@ export const AgentBulkActions: React.FunctionComponent<{ {isUnenrollModalOpen && ( { setIsUnenrollModalOpen(false); refreshAgents(); @@ -179,10 +186,8 @@ export const AgentBulkActions: React.FunctionComponent<{ { setIsUpgradeModalOpen(false); refreshAgents(); @@ -230,12 +235,9 @@ export const AgentBulkActions: React.FunctionComponent<{ > @@ -248,9 +250,7 @@ export const AgentBulkActions: React.FunctionComponent<{ - {selectionMode === 'manual' && - selectedAgents.length === selectableAgents && - selectableAgents < totalAgents ? ( + {showSelectEverything ? (

    )} + {ccsWarning && ( +
    + +

    + {i18n.translate('xpack.ml.dataGrid.CcsWarningCalloutBody', { + defaultMessage: + 'There was an issue retrieving data for the index pattern. Source preview in combination with cross-cluster search is only supported for versions 7.10 and above. You may still configure and create the transform.', + })} +

    +
    + +
    + )}
    void; rowCount: number; rowCountRelation: RowCountRelation; + setCcsWarning: Dispatch>; setColumnCharts: Dispatch>; setErrorMessage: Dispatch>; setNoDataMessage: Dispatch>; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index e62f2eb2f003b..633c3d9aab002 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -36,6 +36,7 @@ export const useDataGrid = ( ): UseDataGridReturnType => { const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + const [ccsWarning, setCcsWarning] = useState(false); const [noDataMessage, setNoDataMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); @@ -152,6 +153,7 @@ export const useDataGrid = ( }, [chartsVisible, rowCount, rowCountRelation]); return { + ccsWarning, chartsVisible, chartsButtonVisible: true, columnsWithCharts, @@ -166,6 +168,7 @@ export const useDataGrid = ( rowCount, rowCountRelation, setColumnCharts, + setCcsWarning, setErrorMessage, setNoDataMessage, setPagination, diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index bc76020d19649..540fa65bf6c18 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -331,6 +331,7 @@ export const ScatterplotMatrix: FC = ({ fullWidth > = ({ fullWidth > { if (fieldId === EVENT_RATE_FIELD_ID) return false; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx index d9f1d78c302fd..5b8fc82ef587b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx @@ -38,6 +38,7 @@ const COPY_TO_CLIPBOARD_RUNTIME_MAPPINGS = i18n.translate( const { useXJsonMode } = XJson; const xJsonMode = new XJsonMode(); +export type XJsonModeType = ReturnType; interface Props { actions: CreateAnalyticsFormProps['actions']; @@ -130,7 +131,7 @@ export const RuntimeMappings: FC = ({ actions, state }) => { defaultMessage: 'Runtime mappings', })} > - + {isPopulatedObject(runtimeMappings) ? ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx index 70544cc14ba08..66a96e7316e8a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx @@ -10,6 +10,7 @@ import React, { memo, FC } from 'react'; import { EuiCodeEditor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isRuntimeMappings } from '../../../../../../../common/util/runtime_field_utils'; +import { XJsonModeType } from './runtime_mappings'; interface Props { convertToJson: (data: string) => string; @@ -17,7 +18,7 @@ interface Props { setIsRuntimeMappingsEditorApplyButtonEnabled: React.Dispatch>; advancedEditorRuntimeMappingsLastApplied: string | undefined; advancedRuntimeMappingsConfig: string; - xJsonMode: any; + xJsonMode: XJsonModeType; } export const RuntimeMappingsEditor: FC = memo( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index f48f4a62f5a7d..2d9ae1cd4689b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -13,7 +13,7 @@ import { CoreSetup } from 'src/core/public'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils'; -import { RuntimeMappings, RuntimeField } from '../../../../../../common/types/fields'; +import { RuntimeMappings } from '../../../../../../common/types/fields'; import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms'; import { DataLoader } from '../../../../datavisualizer/index_based/data_loader'; @@ -44,7 +44,7 @@ interface MLEuiDataGridColumn extends EuiDataGridColumn { function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) { return Object.keys(runtimeMappings).map((id) => { const field = runtimeMappings[id]; - const schema = getDataGridSchemaFromESFieldType(field.type as RuntimeField['type']); + const schema = getDataGridSchemaFromESFieldType(field.type as estypes.RuntimeField['type']); return { id, schema, isExpandable: schema !== 'boolean', isRuntimeFieldColumn: true }; }); } @@ -64,7 +64,7 @@ export const useIndexData = ( const field = indexPattern.fields.getByName(id); const isRuntimeFieldColumn = field?.runtimeField !== undefined; const schema = isRuntimeFieldColumn - ? getDataGridSchemaFromESFieldType(field?.type as RuntimeField['type']) + ? getDataGridSchemaFromESFieldType(field?.type as estypes.RuntimeField['type']) : getDataGridSchemaFromKibanaFieldType(field); return { id, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index e848f209516f4..3795af32f6638 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -34,6 +34,7 @@ import { DataFrameTaskStateType } from '../../../analytics_management/components import { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '../expandable_section'; +import { EvaluateStat } from './evaluate_stat'; import { getRocCurveChartVegaLiteSpec } from './get_roc_curve_chart_vega_lite_spec'; @@ -112,10 +113,12 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se const isTraining = isTrainingFilter(searchQuery, resultsField); const { + avgRecall, confusionMatrixData, docsCount, error: errorConfusionMatrix, isLoading: isLoadingConfusionMatrix, + overallAccuracy, } = useConfusionMatrix(jobConfig, searchQuery); useEffect(() => { @@ -368,8 +371,52 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se )} ) : null} + {/* Accuracy and Recall */} + + + + + + + + + {/* AUC ROC Chart */} - + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx new file mode 100644 index 0000000000000..4bb8415d833f6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EMPTY_STAT } from '../../../../common/analytics'; + +interface Props { + isLoading: boolean; + title: number | null; + description: string; + dataTestSubj: string; + tooltipContent: string; +} + +export const EvaluateStat: FC = ({ + isLoading, + title, + description, + dataTestSubj, + tooltipContent, +}) => ( + + + + + + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index be44a8e36ed00..df48d2c5ab44f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -30,6 +30,8 @@ export const useConfusionMatrix = ( searchQuery: ResultsSearchQuery ) => { const [confusionMatrixData, setConfusionMatrixData] = useState([]); + const [overallAccuracy, setOverallAccuracy] = useState(null); + const [avgRecall, setAvgRecall] = useState(null); const [isLoading, setIsLoading] = useState(false); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); @@ -77,6 +79,8 @@ export const useConfusionMatrix = ( evalData.eval?.classification?.multiclass_confusion_matrix?.confusion_matrix; setError(null); setConfusionMatrixData(confusionMatrix || []); + setAvgRecall(evalData.eval?.classification?.recall?.avg_recall || null); + setOverallAccuracy(evalData.eval?.classification?.accuracy?.overall_accuracy || null); setIsLoading(false); } else { setIsLoading(false); @@ -94,5 +98,5 @@ export const useConfusionMatrix = ( loadConfusionMatrixData(); }, [JSON.stringify([jobConfig, searchQuery])]); - return { confusionMatrixData, docsCount, error, isLoading }; + return { avgRecall, confusionMatrixData, docsCount, error, isLoading, overallAccuracy }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index 5e508df7c6ae5..7080d86498a51 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, Axis, AxisStyle, Chart, @@ -137,7 +137,7 @@ export const DecisionPathChart = ({ {regressionBaselineData && ( { + const SavedObjectFinderUi = ({ + onChoose, + }: { + onChoose: (id: string, type: string, fullName: string, savedObject: object) => void; + }) => { + return ( + <> + + + + + + ); + }; + + return { + SavedObjectFinderUi, + }; +}); + +const mockNavigateToPath = jest.fn(); +jest.mock('../../../../../contexts/kibana', () => ({ + useMlKibana: () => ({ + services: { + savedObjects: {}, + uiSettings: {}, + }, + }), + useNavigateToPath: () => mockNavigateToPath, +})); + +jest.mock('../../../../../util/index_utils', () => { + return { + getIndexPatternAndSavedSearch: jest.fn( + async (id: string): Promise => { + return { + indexPattern: { + fields: [], + title: + id === 'the-remote-saved-search-id' + ? 'my_remote_cluster:index-pattern-title' + : 'index-pattern-title', + }, + savedSearch: null, + }; + } + ), + }; +}); + +const mockOnClose = jest.fn(); +const mockGetIndexPatternAndSavedSearch = getIndexPatternAndSavedSearch as jest.Mock; + +describe('Data Frame Analytics: ', () => { + afterEach(() => { + mockNavigateToPath.mockClear(); + mockGetIndexPatternAndSavedSearch.mockClear(); + }); + + it('renders the title text', async () => { + // prepare + render( + + + + ); + + // assert + expect(screen.queryByText('New analytics job')).toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledTimes(0); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0); + }); + + it('shows the error callout when clicking a remote index pattern', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('RemoteIndexPattern', { selector: 'button' })); + await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut')); + + // assert + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledTimes(0); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0); + }); + + it('calls navigateToPath for a plain index pattern ', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('PlainIndexPattern', { selector: 'button' })); + + // assert + await waitFor(() => { + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).not.toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledWith( + '/data_frame_analytics/new_job?index=the-plain-index-pattern-id' + ); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0); + }); + }); + + it('shows the error callout when clicking a saved search using a remote index pattern', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('RemoteSavedSearch', { selector: 'button' })); + await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut')); + + // assert + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).toBeInTheDocument(); + expect( + screen.queryByText( + `The saved search 'the-remote-saved-search-title' uses the index pattern 'my_remote_cluster:index-pattern-title'.` + ) + ).toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledTimes(0); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-remote-saved-search-id'); + }); + + it('calls navigateToPath for a saved search using a plain index pattern ', async () => { + // prepare + render( + + + + ); + + // act + fireEvent.click(screen.getByText('PlainSavedSearch', { selector: 'button' })); + + // assert + await waitFor(() => { + expect( + screen.queryByText('Index patterns using cross-cluster search are not supported.') + ).not.toBeInTheDocument(); + expect(mockNavigateToPath).toHaveBeenCalledWith( + '/data_frame_analytics/new_job?savedSearchId=the-plain-saved-search-id' + ); + expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-plain-saved-search-id'); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index 40f97690d7790..cbc5a226eb319 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -5,15 +5,28 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; +import { + EuiCallOut, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, +} from '@elastic/eui'; + +import type { SimpleSavedObject } from 'src/core/public'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; +import { getNestedProperty } from '../../../../../util/object_utils'; + +import { getIndexPatternAndSavedSearch } from '../../../../../util/index_utils'; + const fixedPageSize: number = 8; interface Props { @@ -26,7 +39,49 @@ export const SourceSelection: FC = ({ onClose }) => { } = useMlKibana(); const navigateToPath = useNavigateToPath(); - const onSearchSelected = async (id: string, type: string) => { + const [isCcsCallOut, setIsCcsCallOut] = useState(false); + const [ccsCallOutBodyText, setCcsCallOutBodyText] = useState(); + + const onSearchSelected = async ( + id: string, + type: string, + fullName: string, + savedObject: SimpleSavedObject + ) => { + // Kibana index patterns including `:` are cross-cluster search indices + // and are not supported by Data Frame Analytics yet. For saved searches + // and index patterns that use cross-cluster search we intercept + // the selection before redirecting and show an error callout instead. + let indexPatternTitle = ''; + + if (type === 'index-pattern') { + indexPatternTitle = getNestedProperty(savedObject, 'attributes.title'); + } else if (type === 'search') { + const indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(id); + indexPatternTitle = indexPatternAndSavedSearch.indexPattern?.title ?? ''; + } + + if (indexPatternTitle.includes(':')) { + setIsCcsCallOut(true); + if (type === 'search') { + setCcsCallOutBodyText( + i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutBody', + { + defaultMessage: `The saved search '{savedSearchTitle}' uses the index pattern '{indexPatternTitle}'.`, + values: { + savedSearchTitle: getNestedProperty(savedObject, 'attributes.title'), + indexPatternTitle, + }, + } + ) + ); + } else { + setCcsCallOutBodyText(undefined); + } + return; + } + await navigateToPath( `/data_frame_analytics/new_job?${ type === 'index-pattern' ? 'index' : 'savedSearchId' @@ -54,6 +109,23 @@ export const SourceSelection: FC = ({ onClose }) => { + {isCcsCallOut && ( + <> + + {typeof ccsCallOutBodyText === 'string' &&

    {ccsCallOutBodyText}

    } +
    + + + )} any>(func: T, context?: any) => { const memoizedLoadAnnotationsTableData = memoize( loadAnnotationsTableData ); -const memoizedLoadDataForCharts = memoize(loadDataForCharts); const memoizedLoadFilteredTopInfluencers = memoize( loadFilteredTopInfluencers ); @@ -96,7 +95,7 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi const loadExplorerDataProvider = ( mlResultsService: MlResultsService, anomalyTimelineService: AnomalyTimelineService, - anomalyExplorerService: AnomalyExplorerChartsService, + anomalyExplorerChartsService: AnomalyExplorerChartsService, timefilter: TimefilterContract ) => { const memoizedLoadOverallData = memoize( @@ -108,8 +107,8 @@ const loadExplorerDataProvider = ( anomalyTimelineService ); const memoizedAnomalyDataChange = memoize( - anomalyExplorerService.getAnomalyData, - anomalyExplorerService + anomalyExplorerChartsService.getAnomalyData, + anomalyExplorerChartsService ); return (config: LoadExplorerDataConfig): Observable> => { @@ -160,9 +159,7 @@ const loadExplorerDataProvider = ( swimlaneBucketInterval.asSeconds(), bounds ), - anomalyChartRecords: memoizedLoadDataForCharts( - lastRefresh, - mlResultsService, + anomalyChartRecords: anomalyExplorerChartsService.loadDataForCharts$( jobIds, timerange.earliestMs, timerange.latestMs, @@ -214,42 +211,30 @@ const loadExplorerDataProvider = ( // show the view-by loading indicator // and pass on the data we already fetched. tap(explorerService.setViewBySwimlaneLoading), - // Trigger a side-effect to update the charts. - tap(({ anomalyChartRecords, topFieldValues }) => { - if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) { - memoizedAnomalyDataChange( - lastRefresh, - explorerService, - combinedJobRecords, - swimlaneContainerWidth, - anomalyChartRecords, - timerange.earliestMs, - timerange.latestMs, - timefilter, - tableSeverity - ); - } else { - memoizedAnomalyDataChange( - lastRefresh, - explorerService, - combinedJobRecords, - swimlaneContainerWidth, - [], - timerange.earliestMs, - timerange.latestMs, - timefilter, - tableSeverity - ); - } - }), - // Load view-by swimlane data and filtered top influencers. - // mergeMap is used to have access to the already fetched data and act on it in arg #1. - // In arg #2 of mergeMap we combine the data and pass it on in the action format - // which can be consumed by explorerReducer() later on. + tap(explorerService.setChartsDataLoading), mergeMap( - ({ anomalyChartRecords, influencers, overallState, topFieldValues }) => + ({ + anomalyChartRecords, + influencers, + overallState, + topFieldValues, + annotationsData, + tableData, + }) => forkJoin({ - influencers: + anomalyChartsData: memoizedAnomalyDataChange( + lastRefresh, + combinedJobRecords, + swimlaneContainerWidth, + selectedCells !== undefined && Array.isArray(anomalyChartRecords) + ? anomalyChartRecords + : [], + timerange.earliestMs, + timerange.latestMs, + timefilter, + tableSeverity + ), + filteredTopInfluencers: (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && anomalyChartRecords !== undefined && anomalyChartRecords.length > 0 @@ -280,24 +265,26 @@ const loadExplorerDataProvider = ( swimlaneContainerWidth, influencersFilterQuery ), - }), - ( - { annotationsData, overallState, tableData }, - { influencers, viewBySwimlaneState } - ): Partial => { - return { - annotations: annotationsData, - influencers: influencers as any, - loading: false, - viewBySwimlaneDataLoading: false, - overallSwimlaneData: overallState, - viewBySwimlaneData: viewBySwimlaneState as any, - tableData, - swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) - ? viewBySwimlaneState.cardinality - : undefined, - }; - } + }).pipe( + tap(({ anomalyChartsData }) => { + explorerService.setCharts(anomalyChartsData as ExplorerChartsData); + }), + map(({ viewBySwimlaneState, filteredTopInfluencers }) => { + return { + annotations: annotationsData, + influencers: filteredTopInfluencers as any, + loading: false, + viewBySwimlaneDataLoading: false, + anomalyChartsDataLoading: false, + overallSwimlaneData: overallState, + viewBySwimlaneData: viewBySwimlaneState as any, + tableData, + swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) + ? viewBySwimlaneState.cardinality + : undefined, + }; + }) + ) ) ); }; @@ -319,7 +306,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any) uiSettings, mlResultsService ); - const anomalyExplorerService = new AnomalyExplorerChartsService( + const anomalyExplorerChartsService = new AnomalyExplorerChartsService( timefilter, mlApiServices, mlResultsService @@ -327,7 +314,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any) return loadExplorerDataProvider( mlResultsService, anomalyTimelineService, - anomalyExplorerService, + anomalyExplorerChartsService, timefilter ); }, []); diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx deleted file mode 100644 index 8fe2c32b766b4..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ /dev/null @@ -1,312 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC, useCallback, useMemo, useState, useEffect } from 'react'; -import { debounce } from 'lodash'; -import { - EuiFormRow, - EuiCheckboxGroup, - EuiInMemoryTableProps, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, - EuiButtonEmpty, - EuiButton, - EuiModalFooter, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiModalBody } from '@elastic/eui'; -import { EuiInMemoryTable } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../contexts/kibana'; -import { DashboardSavedObject } from '../../../../../../src/plugins/dashboard/public'; -import { getDefaultSwimlanePanelTitle } from '../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; -import { useDashboardService } from '../services/dashboard_service'; -import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; -import { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../embeddables'; - -export interface DashboardItem { - id: string; - title: string; - description: string | undefined; - attributes: DashboardSavedObject; -} - -export type EuiTableProps = EuiInMemoryTableProps; - -function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { - return { - type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - title: getDefaultSwimlanePanelTitle(jobIds), - }; -} - -interface AddToDashboardControlProps { - jobIds: JobId[]; - viewBy: string; - onClose: (callback?: () => Promise) => void; -} - -/** - * Component for attaching anomaly swim lane embeddable to dashboards. - */ -export const AddToDashboardControl: FC = ({ - onClose, - jobIds, - viewBy, -}) => { - const { - notifications: { toasts }, - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); - - useEffect(() => { - fetchDashboards(); - - return () => { - fetchDashboards.cancel(); - }; - }, []); - - const dashboardService = useDashboardService(); - - const [isLoading, setIsLoading] = useState(false); - const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({ - [SWIMLANE_TYPE.OVERALL]: true, - [SWIMLANE_TYPE.VIEW_BY]: false, - }); - const [dashboardItems, setDashboardItems] = useState([]); - const [selectedItems, setSelectedItems] = useState([]); - - const fetchDashboards = useCallback( - debounce(async (query?: string) => { - try { - const response = await dashboardService.fetchDashboards(query); - const items: DashboardItem[] = response.savedObjects.map((savedObject) => { - return { - id: savedObject.id, - title: savedObject.attributes.title, - description: savedObject.attributes.description, - attributes: savedObject.attributes, - }; - }); - setDashboardItems(items); - } catch (e) { - toasts.danger({ - body: e, - }); - } - setIsLoading(false); - }, 500), - [] - ); - - const search: EuiTableProps['search'] = useMemo(() => { - return { - onChange: ({ queryText }) => { - setIsLoading(true); - fetchDashboards(queryText); - }, - box: { - incremental: true, - 'data-test-subj': 'mlDashboardsSearchBox', - }, - }; - }, []); - - const addSwimlaneToDashboardCallback = useCallback(async () => { - const swimlanes = Object.entries(selectedSwimlanes) - .filter(([, isSelected]) => isSelected) - .map(([swimlaneType]) => swimlaneType); - - for (const selectedDashboard of selectedItems) { - const panelsData = swimlanes.map((swimlaneType) => { - const config = getDefaultEmbeddablePanelConfig(jobIds); - if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) { - return { - ...config, - embeddableConfig: { - jobIds, - swimlaneType, - viewBy, - }, - }; - } - return { - ...config, - embeddableConfig: { - jobIds, - swimlaneType, - }, - }; - }); - - try { - await dashboardService.attachPanels( - selectedDashboard.id, - selectedDashboard.attributes, - panelsData - ); - toasts.success({ - title: ( - - ), - toastLifeTimeMs: 3000, - }); - } catch (e) { - toasts.danger({ - body: e, - }); - } - } - }, [selectedSwimlanes, selectedItems]); - - const columns: EuiTableProps['columns'] = [ - { - field: 'title', - name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', { - defaultMessage: 'Title', - }), - sortable: true, - truncateText: true, - }, - { - field: 'description', - name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', { - defaultMessage: 'Description', - }), - truncateText: true, - }, - ]; - - const swimlaneTypeOptions = [ - { - id: SWIMLANE_TYPE.OVERALL, - label: i18n.translate('xpack.ml.explorer.overallLabel', { - defaultMessage: 'Overall', - }), - }, - { - id: SWIMLANE_TYPE.VIEW_BY, - label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { - defaultMessage: 'View by {viewByField}', - values: { viewByField: viewBy }, - }), - }, - ]; - - const selection: EuiTableProps['selection'] = { - onSelectionChange: setSelectedItems, - }; - - const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); - - return ( - - - - - - - - - } - > - { - const newSelection = { - ...selectedSwimlanes, - [optionId]: !selectedSwimlanes[optionId as SwimlaneType], - }; - setSelectedSwimlanes(newSelection); - }} - data-test-subj="mlAddToDashboardSwimlaneTypeSelector" - /> - - - - - - } - data-test-subj="mlDashboardSelectionContainer" - > - - - - - - - - { - onClose(async () => { - const selectedDashboardId = selectedItems[0].id; - await addSwimlaneToDashboardCallback(); - await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); - }); - }} - data-test-subj="mlAddAndEditDashboardButton" - > - - - - - - - - ); -}; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx new file mode 100644 index 0000000000000..9f65449169ee6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, FC } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../contexts/kibana'; +import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils'; +import { TimeRangeBounds } from '../util/time_buckets'; +import { AddAnomalyChartsToDashboardControl } from './dashboard_controls/add_anomaly_charts_to_dashboard_controls'; + +interface AnomalyContextMenuProps { + selectedJobs: ExplorerJob[]; + selectedCells?: AppStateSelectedCells; + bounds?: TimeRangeBounds; + interval?: number; + chartsCount: number; +} +export const AnomalyContextMenu: FC = ({ + selectedJobs, + selectedCells, + bounds, + interval, + chartsCount, +}) => { + const { + services: { + application: { capabilities }, + }, + } = useMlKibana(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAddDashboardsActive, setIsAddDashboardActive] = useState(false); + + const canEditDashboards = capabilities.dashboard?.createNew ?? false; + const menuItems = useMemo(() => { + const items = []; + if (canEditDashboards) { + items.push( + + + + ); + } + return items; + }, [canEditDashboards]); + + const jobIds = selectedJobs.map(({ id }) => id); + + return ( + <> + {menuItems.length > 0 && ( + + + } + isOpen={isMenuOpen} + closePopover={setIsMenuOpen.bind(null, false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + )} + {isAddDashboardsActive && selectedJobs && ( + { + setIsAddDashboardActive(false); + if (callback) { + await callback(); + } + }} + selectedCells={selectedCells} + bounds={bounds} + interval={interval} + jobIds={jobIds} + /> + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 7c63d4087ce1e..37967d18dbbd9 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -24,7 +24,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; -import { AddToDashboardControl } from './add_to_dashboard_control'; +import { AddSwimlaneToDashboardControl } from './dashboard_controls/add_swimlane_to_dashboard_controls'; import { useMlKibana } from '../contexts/kibana'; import { TimeBuckets } from '../util/time_buckets'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; @@ -294,7 +294,7 @@ export const AnomalyTimeline: FC = React.memo( )} {isAddDashboardsActive && selectedJobs && ( - { setIsAddDashboardActive(false); if (callback) { diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..5c3c6edee59c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFieldNumber, EuiFormRow, formatDate } from '@elastic/eui'; +import { useDashboardTable } from './use_dashboards_table'; +import { AddToDashboardControl } from './add_to_dashboard_controls'; +import { useAddToDashboardActions } from './use_add_to_dashboard_actions'; +import { AppStateSelectedCells, getSelectionTimeRange } from '../explorer_utils'; +import { TimeRange } from '../../../../../../../src/plugins/data/common/query'; +import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../services/anomaly_explorer_charts_service'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../../embeddables'; +import { getDefaultExplorerChartsPanelTitle } from '../../../embeddables/anomaly_charts/anomaly_charts_embeddable'; +import { TimeRangeBounds } from '../../util/time_buckets'; +import { useTableSeverity } from '../../components/controls/select_severity'; +import { MAX_ANOMALY_CHARTS_ALLOWED } from '../../../embeddables/anomaly_charts/anomaly_charts_initializer'; + +function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { + return { + type: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, + title: getDefaultExplorerChartsPanelTitle(jobIds), + }; +} + +export interface AddToDashboardControlProps { + jobIds: string[]; + selectedCells?: AppStateSelectedCells; + bounds?: TimeRangeBounds; + interval?: number; + onClose: (callback?: () => Promise) => void; +} + +/** + * Component for attaching anomaly swim lane embeddable to dashboards. + */ +export const AddAnomalyChartsToDashboardControl: FC = ({ + onClose, + jobIds, + selectedCells, + bounds, + interval, +}) => { + const [severity] = useTableSeverity(); + const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(DEFAULT_MAX_SERIES_TO_PLOT); + + const getPanelsData = useCallback(async () => { + let timeRange: TimeRange | undefined; + if (selectedCells !== undefined && interval !== undefined && bounds !== undefined) { + const { earliestMs, latestMs } = getSelectionTimeRange(selectedCells, interval, bounds); + timeRange = { + from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'), + to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'), + mode: 'absolute', + }; + } + + const config = getDefaultEmbeddablePanelConfig(jobIds); + return [ + { + ...config, + embeddableConfig: { + jobIds, + maxSeriesToPlot: maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT, + severityThreshold: severity.val, + ...(timeRange ?? {}), + }, + }, + ]; + }, [selectedCells, interval, bounds, jobIds, maxSeriesToPlot, severity]); + + const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable(); + const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({ + onClose, + getPanelsData, + selectedDashboards: selectedItems, + }); + const title = ( + + ); + + const disabled = selectedItems.length < 1 && !Array.isArray(jobIds === undefined); + + const extraControls = ( + + } + > + setMaxSeriesToPlot(parseInt(e.target.value, 10))} + min={0} + max={MAX_ANOMALY_CHARTS_ALLOWED} + /> + + ); + + return ( + + {extraControls} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..79089e7e5baf9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback, useState } from 'react'; +import { EuiFormRow, EuiCheckboxGroup, EuiInMemoryTableProps, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { DashboardSavedObject } from '../../../../../../../src/plugins/dashboard/public'; +import { getDefaultSwimlanePanelTitle } from '../../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; +import { SWIMLANE_TYPE, SwimlaneType } from '../explorer_constants'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../../embeddables'; +import { useDashboardTable } from './use_dashboards_table'; +import { AddToDashboardControl } from './add_to_dashboard_controls'; +import { useAddToDashboardActions } from './use_add_to_dashboard_actions'; + +export interface DashboardItem { + id: string; + title: string; + description: string | undefined; + attributes: DashboardSavedObject; +} + +export type EuiTableProps = EuiInMemoryTableProps; + +function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { + return { + type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + title: getDefaultSwimlanePanelTitle(jobIds), + }; +} + +interface AddToDashboardControlProps { + jobIds: JobId[]; + viewBy: string; + onClose: (callback?: () => Promise) => void; +} + +/** + * Component for attaching anomaly swim lane embeddable to dashboards. + */ +export const AddSwimlaneToDashboardControl: FC = ({ + onClose, + jobIds, + viewBy, +}) => { + const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable(); + + const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({ + [SWIMLANE_TYPE.OVERALL]: true, + [SWIMLANE_TYPE.VIEW_BY]: false, + }); + + const getPanelsData = useCallback(async () => { + const swimlanes = Object.entries(selectedSwimlanes) + .filter(([, isSelected]) => isSelected) + .map(([swimlaneType]) => swimlaneType); + + return swimlanes.map((swimlaneType) => { + const config = getDefaultEmbeddablePanelConfig(jobIds); + if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) { + return { + ...config, + embeddableConfig: { + jobIds, + swimlaneType, + viewBy, + }, + }; + } + return { + ...config, + embeddableConfig: { + jobIds, + swimlaneType, + }, + }; + }); + }, [selectedSwimlanes, selectedItems]); + const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({ + onClose, + getPanelsData, + selectedDashboards: selectedItems, + }); + + const swimlaneTypeOptions = [ + { + id: SWIMLANE_TYPE.OVERALL, + label: i18n.translate('xpack.ml.explorer.overallLabel', { + defaultMessage: 'Overall', + }), + }, + { + id: SWIMLANE_TYPE.VIEW_BY, + label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { + defaultMessage: 'View by {viewByField}', + values: { viewByField: viewBy }, + }), + }, + ]; + + const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); + + const extraControls = ( + <> + + } + > + { + const newSelection = { + ...selectedSwimlanes, + [optionId]: !selectedSwimlanes[optionId as SwimlaneType], + }; + setSelectedSwimlanes(newSelection); + }} + data-test-subj="mlAddToDashboardSwimlaneTypeSelector" + /> + + + + ); + + const title = ( + + ); + + const disabled = noSwimlaneSelected || selectedItems.length === 0; + return ( + + {extraControls} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..7806e531834a1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFormRow, + EuiInMemoryTable, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTableProps, useDashboardTable } from './use_dashboards_table'; + +export const columns: EuiTableProps['columns'] = [ + { + field: 'title', + name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', { + defaultMessage: 'Title', + }), + sortable: true, + truncateText: true, + }, + { + field: 'description', + name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', { + defaultMessage: 'Description', + }), + truncateText: true, + }, +]; + +interface AddToDashboardControlProps extends ReturnType { + onClose: (callback?: () => Promise) => void; + addToDashboardAndEditCallback: () => Promise; + addToDashboardCallback: () => Promise; + title: React.ReactNode; + disabled: boolean; + children?: React.ReactElement; +} +export const AddToDashboardControl: FC = ({ + onClose, + selection, + dashboardItems, + isLoading, + search, + addToDashboardAndEditCallback, + addToDashboardCallback, + title, + disabled, + children, +}) => { + return ( + + + {title} + + + {children} + + } + data-test-subj="mlDashboardSelectionContainer" + > + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx new file mode 100644 index 0000000000000..82c699865f2e4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DashboardItem } from './use_dashboards_table'; +import { SavedDashboardPanel } from '../../../../../../../src/plugins/dashboard/common/types'; +import { useMlKibana } from '../../contexts/kibana'; +import { useDashboardService } from '../../services/dashboard_service'; + +export const useAddToDashboardActions = ({ + onClose, + getPanelsData, + selectedDashboards, +}: { + onClose: (callback?: () => Promise) => void; + getPanelsData: ( + selectedDashboards: DashboardItem[] + ) => Promise>>; + selectedDashboards: DashboardItem[]; +}) => { + const { + notifications: { toasts }, + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + const dashboardService = useDashboardService(); + + const addToDashboardCallback = useCallback(async () => { + const panelsData = await getPanelsData(selectedDashboards); + for (const selectedDashboard of selectedDashboards) { + try { + await dashboardService.attachPanels( + selectedDashboard.id, + selectedDashboard.attributes, + panelsData + ); + toasts.success({ + title: ( + + ), + toastLifeTimeMs: 3000, + }); + } catch (e) { + toasts.danger({ + body: e, + }); + } + } + }, [selectedDashboards, getPanelsData]); + + const addToDashboardAndEditCallback = useCallback(async () => { + onClose(async () => { + await addToDashboardCallback(); + const selectedDashboardId = selectedDashboards[0].id; + await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); + }); + }, [addToDashboardCallback, selectedDashboards, navigateToUrl]); + + return { addToDashboardCallback, addToDashboardAndEditCallback }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx new file mode 100644 index 0000000000000..8721de497eedc --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiInMemoryTableProps } from '@elastic/eui'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { debounce } from 'lodash'; +import type { DashboardSavedObject } from '../../../../../../../src/plugins/dashboard/public'; +import { useDashboardService } from '../../services/dashboard_service'; +import { useMlKibana } from '../../contexts/kibana'; + +export interface DashboardItem { + id: string; + title: string; + description: string | undefined; + attributes: DashboardSavedObject; +} + +export type EuiTableProps = EuiInMemoryTableProps; + +export const useDashboardTable = () => { + const { + notifications: { toasts }, + } = useMlKibana(); + + const dashboardService = useDashboardService(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + fetchDashboards(); + + return () => { + fetchDashboards.cancel(); + }; + }, []); + + const search: EuiTableProps['search'] = useMemo(() => { + return { + onChange: ({ queryText }) => { + setIsLoading(true); + fetchDashboards(queryText); + }, + box: { + incremental: true, + 'data-test-subj': 'mlDashboardsSearchBox', + }, + }; + }, []); + + const [dashboardItems, setDashboardItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + + const fetchDashboards = useCallback( + debounce(async (query?: string) => { + try { + const response = await dashboardService.fetchDashboards(query); + const items: DashboardItem[] = response.savedObjects.map((savedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + description: savedObject.attributes.description, + attributes: savedObject.attributes, + }; + }); + setDashboardItems(items); + } catch (e) { + toasts.danger({ + body: e, + }); + } + setIsLoading(false); + }, 500), + [] + ); + const selection: EuiTableProps['selection'] = { + onSelectionChange: setSelectedItems, + }; + return { dashboardItems, selectedItems, selection, search, isLoading }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 6979277c43077..45665b2026db5 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -72,6 +72,7 @@ import { getToastNotifications } from '../util/dependency_cache'; import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator'; +import { AnomalyContextMenu } from './anomaly_context_menu'; const ExplorerPage = ({ children, @@ -431,14 +432,32 @@ export class ExplorerUI extends React.Component { )} {loading === false && ( - -

    - + + +

    + +

    +
    +
    + + + -

    -
    +
    +
    { + explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); + }, clearInfluencerFilterSettings: () => { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS }); }, @@ -137,6 +140,9 @@ export const explorerService = { setFilterData: (payload: Partial>) => { explorerAction$.next(setFilterDataActionCreator(payload)); }, + setChartsDataLoading: () => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING }); + }, setSwimlaneContainerWidth: (payload: number) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts index 9e24a4349584e..b410449218d02 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -12,6 +12,7 @@ import { TimeRangeBounds } from '../util/time_buckets'; import { RecordForInfluencer } from '../services/results_service/results_service'; import { InfluencersFilterQuery } from '../../../common/types/es_client'; import { MlResultsService } from '../services/results_service'; +import { EntityField } from '../../../common/util/anomaly_utils'; interface ClearedSelectedAnomaliesState { selectedCells: undefined; @@ -60,7 +61,7 @@ export declare const getSelectionJobIds: ( export declare const getSelectionInfluencers: ( selectedCells: AppStateSelectedCells | undefined, fieldName: string -) => string[]; +) => EntityField[]; interface SelectionTimeRange { earliestMs: number; @@ -149,6 +150,7 @@ export declare const loadDataForCharts: ( ) => Promise; export declare const loadFilteredTopInfluencers: ( + mlResultsService: MlResultsService, jobIds: string[], earliestMs: number, latestMs: number, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index ea101d104f783..69bdac060a2dc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -536,65 +536,6 @@ export async function loadAnomaliesTableData( }); } -// track the request to be able to ignore out of date requests -// and avoid race conditions ending up with the wrong charts. -let requestCount = 0; -export async function loadDataForCharts( - mlResultsService, - jobIds, - earliestMs, - latestMs, - influencers = [], - selectedCells, - influencersFilterQuery, - // choose whether or not to keep track of the request that could be out of date - // in Anomaly Explorer this is being used to ignore any request that are out of date - // but in embeddables, we might have multiple requests coming from multiple different panels - takeLatestOnly = true -) { - return new Promise((resolve) => { - // Just skip doing the request when this function - // is called without the minimum required data. - if ( - selectedCells === undefined && - influencers.length === 0 && - influencersFilterQuery === undefined - ) { - resolve([]); - } - - const newRequestCount = ++requestCount; - requestCount = newRequestCount; - - // Load the top anomalies (by record_score) which will be displayed in the charts. - mlResultsService - .getRecordsForInfluencer( - jobIds, - influencers, - 0, - earliestMs, - latestMs, - 500, - influencersFilterQuery - ) - .then((resp) => { - // Ignore this response if it's returned by an out of date promise - if (takeLatestOnly && newRequestCount < requestCount) { - resolve([]); - } - - if ( - (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || - influencersFilterQuery !== undefined - ) { - resolve(resp.records); - } - - resolve([]); - }); - }); -} - export async function loadTopInfluencers( mlResultsService, selectedJobIds, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index f66cd94314608..15e0caa29af39 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -20,7 +20,7 @@ import { import { checkSelectedCells } from './check_selected_cells'; import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings'; import { jobSelectionChange } from './job_selection_change'; -import { ExplorerState } from './state'; +import { ExplorerState, getExplorerDefaultState } from './state'; import { setInfluencerFilterSettings } from './set_influencer_filter_settings'; import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; import { getTimeBoundsFromSelection } from '../../hooks/use_selected_cells'; @@ -31,6 +31,10 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo let nextState: ExplorerState; switch (type) { + case EXPLORER_ACTION.CLEAR_EXPLORER_DATA: + nextState = getExplorerDefaultState(); + break; + case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS: nextState = clearInfluencerFilterSettings(state); break; @@ -49,6 +53,14 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo nextState = jobSelectionChange(state, payload); break; + case EXPLORER_ACTION.SET_CHARTS_DATA_LOADING: + nextState = { + ...state, + anomalyChartsDataLoading: true, + chartsData: getDefaultChartsData(), + }; + break; + case EXPLORER_ACTION.SET_CHARTS: nextState = { ...state, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index bb90fedfc2315..e9527b7c232e5 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -28,6 +28,7 @@ import { InfluencersFilterQuery } from '../../../../../common/types/es_client'; export interface ExplorerState { annotations: AnnotationsTable; + anomalyChartsDataLoading: boolean; chartsData: ExplorerChartsData; fieldFormatsLoading: boolean; filterActive: boolean; @@ -69,6 +70,7 @@ export function getExplorerDefaultState(): ExplorerState { annotationsData: [], aggregations: {}, }, + anomalyChartsDataLoading: true, chartsData: getDefaultChartsData(), fieldFormatsLoading: false, filterActive: false, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/anomalies.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/anomalies.tsx index fc02f1c427553..f40d5bbd5b013 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/anomalies.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/anomalies.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, FC } from 'react'; -import { AnnotationDomainTypes, LineAnnotation } from '@elastic/charts'; +import { AnnotationDomainType, LineAnnotation } from '@elastic/charts'; import { Anomaly } from '../../../../common/results_loader'; import { getSeverityColor } from '../../../../../../../../common/util/anomaly_utils'; import { ANOMALY_THRESHOLD } from '../../../../../../../../common/constants/anomalies'; @@ -59,35 +59,35 @@ export const Anomalies: FC = ({ anomalyData }) => { = ({ overlayKey, start, end, color, showMar /> = ({ jobsWithTim } }, [JSON.stringify(jobIds)]); + useEffect(() => { + return () => { + // upon component unmounting + // clear any data to prevent next page from rendering old charts + explorerService.clearExplorerData(); + }; + }, []); + /** * TODO get rid of the intermediate state in explorerService. * URL state should be the only source of truth for related props. diff --git a/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts index 21f07ed9e5a3c..28140038d249b 100644 --- a/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts @@ -10,4 +10,5 @@ export const createAnomalyExplorerChartsServiceMock = () => ({ getAnomalyData: jest.fn(), setTimeRange: jest.fn(), getTimeBounds: jest.fn(), + loadDataForCharts$: jest.fn(), }); diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts index 36e18b49cfa84..ac61e11b1128e 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts @@ -13,7 +13,6 @@ import { of } from 'rxjs'; import { cloneDeep } from 'lodash'; import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { ExplorerChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; -import type { ExplorerService } from '../explorer/explorer_dashboard_service'; import type { MlApiServices } from './ml_api_service'; import type { MlResultsService } from './results_service'; import { getDefaultChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; @@ -89,9 +88,6 @@ describe('AnomalyExplorerChartsService', () => { (mlApiServicesMock as unknown) as MlApiServices, (mlResultsServiceMock as unknown) as MlResultsService ); - const explorerService = { - setCharts: jest.fn(), - }; const timeRange = { earliestMs: 1486656000000, @@ -104,13 +100,8 @@ describe('AnomalyExplorerChartsService', () => { ); }); - afterEach(() => { - explorerService.setCharts.mockClear(); - }); - test('should return anomaly data without explorer service', async () => { const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, (combinedJobRecords as unknown) as Record, 1000, mockAnomalyChartRecords, @@ -123,27 +114,8 @@ describe('AnomalyExplorerChartsService', () => { assertAnomalyDataResult(anomalyData); }); - test('should set anomaly data with explorer service side effects', async () => { - await anomalyExplorerService.getAnomalyData( - (explorerService as unknown) as ExplorerService, - (combinedJobRecords as unknown) as Record, - 1000, - mockAnomalyChartRecords, - timeRange.earliestMs, - timeRange.latestMs, - timefilterMock, - 0, - 12 - ); - - expect(explorerService.setCharts.mock.calls.length).toBe(2); - assertAnomalyDataResult(explorerService.setCharts.mock.calls[0][0]); - assertAnomalyDataResult(explorerService.setCharts.mock.calls[1][0]); - }); - test('call anomalyChangeListener with empty series config', async () => { const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, // @ts-ignore (combinedJobRecords as unknown) as Record, 1000, @@ -165,7 +137,6 @@ describe('AnomalyExplorerChartsService', () => { mockAnomalyChartRecordsClone[1].partition_field_value = 'AAL.'; const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, (combinedJobRecords as unknown) as Record, 1000, mockAnomalyChartRecordsClone, diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 72de5d003d4b8..7aff2ff7e0026 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -7,6 +7,8 @@ import { each, find, get, map, reduce, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Observable, of } from 'rxjs'; +import { map as mapObservable } from 'rxjs/operators'; import { RecordForInfluencer } from './results_service/results_service'; import { isMappableJob, @@ -29,7 +31,6 @@ import { CHART_TYPE, ChartType } from '../explorer/explorer_constants'; import type { ChartRecord } from '../explorer/explorer_utils'; import { RecordsForCriteria, ScheduledEventsByBucket } from './results_service/result_service_rx'; import { isPopulatedObject } from '../../../common/util/object_utils'; -import type { ExplorerService } from '../explorer/explorer_dashboard_service'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { ExplorerChartsData, @@ -37,6 +38,8 @@ import { } from '../explorer/explorer_charts/explorer_charts_container_service'; import { TimeRangeBounds } from '../util/time_buckets'; import { isDefined } from '../../../common/types/guards'; +import { AppStateSelectedCells } from '../explorer/explorer_utils'; +import { InfluencersFilterQuery } from '../../../common/types/es_client'; const CHART_MAX_POINTS = 500; const ANOMALIES_MAX_RESULTS = 500; const MAX_SCHEDULED_EVENTS = 10; // Max number of scheduled events displayed per bucket. @@ -370,15 +373,53 @@ export class AnomalyExplorerChartsService { // Getting only necessary job config and datafeed config without the stats jobIds.map((jobId) => this.mlApiServices.jobs.jobForCloning(jobId)) ); - const combinedJobs = combinedResults + return combinedResults .filter(isDefined) .filter((r) => r.job !== undefined && r.datafeed !== undefined) .map(({ job, datafeed }) => ({ ...job, datafeed_config: datafeed } as CombinedJob)); - return combinedJobs; + } + + public loadDataForCharts$( + jobIds: string[], + earliestMs: number, + latestMs: number, + influencers: EntityField[] = [], + selectedCells: AppStateSelectedCells | undefined, + influencersFilterQuery: InfluencersFilterQuery + ): Observable { + if ( + selectedCells === undefined && + influencers.length === 0 && + influencersFilterQuery === undefined + ) { + of([]); + } + + return this.mlResultsService + .getRecordsForInfluencer$( + jobIds, + influencers, + 0, + earliestMs, + latestMs, + 500, + influencersFilterQuery + ) + .pipe( + mapObservable((resp): RecordForInfluencer[] => { + if ( + (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || + influencersFilterQuery !== undefined + ) { + return resp.records; + } + + return [] as RecordForInfluencer[]; + }) + ); } public async getAnomalyData( - explorerService: ExplorerService | undefined, combinedJobRecords: Record, chartsContainerWidth: number, anomalyRecords: ChartRecord[] | undefined, @@ -486,9 +527,6 @@ export class AnomalyExplorerChartsService { data.errorMessages = errorMessages; } - if (explorerService) { - explorerService.setCharts({ ...data }); - } if (seriesConfigs.length === 0) { return data; } @@ -848,9 +886,6 @@ export class AnomalyExplorerChartsService { // push map data in if it's available data.seriesToPlot.push(...mapData); } - if (explorerService) { - explorerService.setCharts({ ...data }); - } return Promise.resolve(data); }) .catch((error) => { @@ -860,7 +895,7 @@ export class AnomalyExplorerChartsService { } public processRecordsForDisplay( - jobRecords: Record, + combinedJobRecords: Record, anomalyRecords: RecordForInfluencer[] ): { records: ChartRecord[]; errors: Record> | undefined } { // Aggregate the anomaly data by detector, and entity (by/over/partition). @@ -875,7 +910,7 @@ export class AnomalyExplorerChartsService { // Check if we can plot a chart for this record, depending on whether the source data // is chartable, and if model plot is enabled for the job. - const job = jobRecords[record.job_id]; + const job = combinedJobRecords[record.job_id]; // if we already know this job has datafeed aggregations we cannot support // no need to do more checks diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index e07d49ca23d3b..caa0e20c3230d 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -22,9 +22,11 @@ import { MlApiServices } from '../ml_api_service'; import { CriteriaField } from './index'; import { findAggField } from '../../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; -import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; +import { aggregationTypeTransform, EntityField } from '../../../../common/util/anomaly_utils'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { isPopulatedObject } from '../../../../common/util/object_utils'; +import { InfluencersFilterQuery } from '../../../../common/types/es_client'; +import { RecordForInfluencer } from './results_service'; interface ResultResponse { success: boolean; @@ -633,5 +635,135 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { latestMs ); }, + + // Queries Elasticsearch to obtain the record level results containing the specified influencer(s), + // for the specified job(s), time range, and record score threshold. + // influencers parameter must be an array, with each object in the array having 'fieldName' + // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, + // so this returns record level results which have at least one of the influencers. + // Pass an empty array or ['*'] to search over all job IDs. + getRecordsForInfluencer$( + jobIds: string[], + influencers: EntityField[], + threshold: number, + earliestMs: number, + latestMs: number, + maxResults: number, + influencersFilterQuery: InfluencersFilterQuery + ): Observable<{ records: RecordForInfluencer[]; success: boolean }> { + const obj = { success: true, records: [] as RecordForInfluencer[] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); + } + + // Add a nested query to filter for each of the specified influencers. + if (influencers.length > 0) { + boolCriteria.push({ + bool: { + should: influencers.map((influencer) => { + return { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': influencer.fieldName, + }, + }, + { + match: { + 'influencers.influencer_field_values': influencer.fieldValue, + }, + }, + ], + }, + }, + }, + }; + }), + minimum_should_match: 1, + }, + }); + } + + return mlApiServices.results + .anomalySearch$( + { + size: maxResults !== undefined ? maxResults : 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }, + jobIds + ) + .pipe( + map((resp) => { + if (resp.hits.total.value > 0) { + each(resp.hits.hits, (hit) => { + obj.records.push(hit._source); + }); + } + return obj; + }) + ); + }, }; } diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index d26e650d145cb..6161eeb4e7940 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -55,7 +55,6 @@ export function resultsServiceProvider( influencersFilterQuery: InfluencersFilterQuery ): Promise; getRecordInfluencers(): Promise; - getRecordsForInfluencer(): Promise; getRecordsForDetector(): Promise; getRecords(): Promise; getEventRateData( diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index b041267f46c04..c258d07cab484 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -779,139 +779,6 @@ export function resultsServiceProvider(mlApiServices) { }); }, - // Queries Elasticsearch to obtain the record level results containing the specified influencer(s), - // for the specified job(s), time range, and record score threshold. - // influencers parameter must be an array, with each object in the array having 'fieldName' - // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, - // so this returns record level results which have at least one of the influencers. - // Pass an empty array or ['*'] to search over all job IDs. - getRecordsForInfluencer( - jobIds, - influencers, - threshold, - earliestMs, - latestMs, - maxResults, - influencersFilterQuery - ) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', - }, - }, - }, - { - range: { - record_score: { - gte: threshold, - }, - }, - }, - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr, - }, - }); - } - - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); - } - - // Add a nested query to filter for each of the specified influencers. - if (influencers.length > 0) { - boolCriteria.push({ - bool: { - should: influencers.map((influencer) => { - return { - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - match: { - 'influencers.influencer_field_name': influencer.fieldName, - }, - }, - { - match: { - 'influencers.influencer_field_values': influencer.fieldValue, - }, - }, - ], - }, - }, - }, - }; - }), - minimum_should_match: 1, - }, - }); - } - - mlApiServices.results - .anomalySearch( - { - size: maxResults !== undefined ? maxResults : 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false, - }, - }, - { - bool: { - must: boolCriteria, - }, - }, - ], - }, - }, - sort: [{ record_score: { order: 'desc' } }], - }, - }, - jobIds - ) - .then((resp) => { - if (resp.hits.total.value > 0) { - each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); - }, - // Queries Elasticsearch to obtain the record level results for the specified job and detector, // time range, record score threshold, and whether to only return results containing influencers. // An additional, optional influencer field name and value may also be provided. @@ -1039,14 +906,6 @@ export function resultsServiceProvider(mlApiServices) { }); }, - // Queries Elasticsearch to obtain all the record level results for the specified job(s), time range, - // and record score threshold. - // Pass an empty array or ['*'] to search over all job IDs. - // Returned response contains a records property, which is an array of the matching results. - getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { - return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); - }, - // Queries Elasticsearch to obtain event rate data i.e. the count // of documents over time. // index can be a String, or String[], of index names to search. diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx index f32446fd6d9ab..a36d063737704 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyChartsEmbeddableInput } from '..'; import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../application/services/anomaly_explorer_charts_service'; -const MAX_SERIES_ALLOWED = 48; +export const MAX_ANOMALY_CHARTS_ALLOWED = 48; export interface AnomalyChartsInitializerProps { defaultTitle: string; initialInput?: Partial>; @@ -98,7 +98,7 @@ export const AnomalyChartsInitializer: FC = ({ value={maxSeriesToPlot} onChange={(e) => setMaxSeriesToPlot(parseInt(e.target.value, 10))} min={0} - max={MAX_SERIES_ALLOWED} + max={MAX_ANOMALY_CHARTS_ALLOWED} /> diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts index efac51edda69f..7045b2eac378a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts @@ -29,41 +29,6 @@ jest.mock('../../application/explorer/explorer_utils', () => ({ }), getSelectionJobIds: jest.fn(() => ['test-job']), getSelectionTimeRange: jest.fn(() => ({ earliestMs: 1521309543000, latestMs: 1616003942999 })), - loadDataForCharts: jest.fn().mockImplementation(() => - Promise.resolve([ - { - job_id: 'cw_multi_1', - result_type: 'record', - probability: 6.057139142746412e-13, - multi_bucket_impact: -5, - record_score: 89.71961, - initial_record_score: 98.36826274948001, - bucket_span: 900, - detector_index: 0, - is_interim: false, - timestamp: 1572892200000, - partition_field_name: 'instance', - partition_field_value: 'i-d17dcd4c', - function: 'mean', - function_description: 'mean', - typical: [1.6177685422858146], - actual: [7.235333333333333], - field_name: 'CPUUtilization', - influencers: [ - { - influencer_field_name: 'region', - influencer_field_values: ['sa-east-1'], - }, - { - influencer_field_name: 'instance', - influencer_field_values: ['i-d17dcd4c'], - }, - ], - instance: ['i-d17dcd4c'], - region: ['sa-east-1'], - }, - ]) - ), })); describe('useAnomalyChartsInputResolver', () => { @@ -115,6 +80,42 @@ describe('useAnomalyChartsInputResolver', () => { }) ); + anomalyExplorerChartsServiceMock.loadDataForCharts$.mockImplementation(() => + Promise.resolve([ + { + job_id: 'cw_multi_1', + result_type: 'record', + probability: 6.057139142746412e-13, + multi_bucket_impact: -5, + record_score: 89.71961, + initial_record_score: 98.36826274948001, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1572892200000, + partition_field_name: 'instance', + partition_field_value: 'i-d17dcd4c', + function: 'mean', + function_description: 'mean', + typical: [1.6177685422858146], + actual: [7.235333333333333], + field_name: 'CPUUtilization', + influencers: [ + { + influencer_field_name: 'region', + influencer_field_values: ['sa-east-1'], + }, + { + influencer_field_name: 'instance', + influencer_field_values: ['i-d17dcd4c'], + }, + ], + instance: ['i-d17dcd4c'], + region: ['sa-east-1'], + }, + ]) + ); + const coreStartMock = createCoreStartMock(); const mlStartMock = createMlStartDepsMock(); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts index b114ca89a3288..703851f3fe9b6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts @@ -18,7 +18,6 @@ import { getSelectionInfluencers, getSelectionJobIds, getSelectionTimeRange, - loadDataForCharts, } from '../../application/explorer/explorer_utils'; import { OVERALL_LABEL, SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; import { parseInterval } from '../../../common/util/parse_interval'; @@ -46,7 +45,7 @@ export function useAnomalyChartsInputResolver( const [ { uiSettings }, { data: dataServices }, - { anomalyDetectorService, anomalyExplorerService, mlResultsService }, + { anomalyDetectorService, anomalyExplorerService }, ] = services; const { timefilter } = dataServices.query.timefilter; @@ -125,15 +124,13 @@ export function useAnomalyChartsInputResolver( const timeRange = getSelectionTimeRange(selections, bucketInterval.asSeconds(), bounds); return forkJoin({ combinedJobs: anomalyExplorerService.getCombinedJobs(jobIds), - anomalyChartRecords: loadDataForCharts( - mlResultsService, + anomalyChartRecords: anomalyExplorerService.loadDataForCharts$( jobIds, timeRange.earliestMs, timeRange.latestMs, selectionInfluencers, selections, - influencersFilterQuery, - false + influencersFilterQuery ), }).pipe( switchMap(({ combinedJobs, anomalyChartRecords }) => { @@ -147,7 +144,6 @@ export function useAnomalyChartsInputResolver( return forkJoin({ chartsData: from( anomalyExplorerService.getAnomalyData( - undefined, combinedJobRecords, embeddableContainerWidth, anomalyChartRecords, diff --git a/x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.ts new file mode 100644 index 0000000000000..e5c6a2345e167 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/__mocks__/ml_url_generator.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator'; +import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; + +export const createMlUrlGeneratorMock = () => + ({ + id: ML_APP_URL_GENERATOR, + isDeprecated: false, + createUrl: jest.fn(), + migrate: jest.fn(), + } as jest.Mocked>); diff --git a/x-pack/plugins/ml/public/mocks.ts b/x-pack/plugins/ml/public/mocks.ts new file mode 100644 index 0000000000000..6b55cb3b6b650 --- /dev/null +++ b/x-pack/plugins/ml/public/mocks.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createMlUrlGeneratorMock } from './ml_url_generator/__mocks__/ml_url_generator'; +import { MlPluginSetup, MlPluginStart } from './plugin'; +const createSetupContract = (): jest.Mocked => { + return { + urlGenerator: createMlUrlGeneratorMock(), + }; +}; + +const createStartContract = (): jest.Mocked => { + return { + urlGenerator: createMlUrlGeneratorMock(), + }; +}; + +export const mlPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/ml/server/lib/spaces_utils.ts b/x-pack/plugins/ml/server/lib/spaces_utils.ts index 5a1d3b89ef57d..21ef163734883 100644 --- a/x-pack/plugins/ml/server/lib/spaces_utils.ts +++ b/x-pack/plugins/ml/server/lib/spaces_utils.ts @@ -5,25 +5,20 @@ * 2.0. */ -import { Legacy } from 'kibana'; import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesPluginStart } from '../../../spaces/server'; import { PLUGIN_ID } from '../../common/constants/app'; -export type RequestFacade = KibanaRequest | Legacy.Request; - export function spacesUtilsProvider( getSpacesPlugin: (() => Promise) | undefined, - request: RequestFacade + request: KibanaRequest ) { async function isMlEnabledInSpace(): Promise { if (getSpacesPlugin === undefined) { // if spaces is disabled force isMlEnabledInSpace to be true return true; } - const space = await (await getSpacesPlugin()).spacesService.getActiveSpace( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); + const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(request); return space.disabledFeatures.includes(PLUGIN_ID) === false; } @@ -31,9 +26,7 @@ export function spacesUtilsProvider( if (getSpacesPlugin === undefined) { return null; } - const client = (await getSpacesPlugin()).spacesService.createSpacesClient( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); + const client = (await getSpacesPlugin()).spacesService.createSpacesClient(request); return await client.getAll(); } @@ -58,9 +51,7 @@ export function spacesUtilsProvider( // if spaces is disabled force isMlEnabledInSpace to be true return null; } - const space = await (await getSpacesPlugin()).spacesService.getActiveSpace( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); + const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(request); return space.id; } diff --git a/x-pack/plugins/ml/server/mocks.ts b/x-pack/plugins/ml/server/mocks.ts new file mode 100644 index 0000000000000..e50f78a0fd99d --- /dev/null +++ b/x-pack/plugins/ml/server/mocks.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createJobServiceProviderMock } from './shared_services/providers/__mocks__/jobs_service'; +import { createAnomalyDetectorsProviderMock } from './shared_services/providers/__mocks__/anomaly_detectors'; +import { createMockMlSystemProvider } from './shared_services/providers/__mocks__/system'; +import { createModulesProviderMock } from './shared_services/providers/__mocks__/modules'; +import { createResultsServiceProviderMock } from './shared_services/providers/__mocks__/results_service'; +import { createAlertingServiceProviderMock } from './shared_services/providers/__mocks__/alerting_service'; +import { MlPluginSetup } from './plugin'; + +const createSetupContract = () => + (({ + jobServiceProvider: createJobServiceProviderMock(), + anomalyDetectorsProvider: createAnomalyDetectorsProviderMock(), + mlSystemProvider: createMockMlSystemProvider(), + modulesProvider: createModulesProviderMock(), + resultsServiceProvider: createResultsServiceProviderMock(), + alertingServiceProvider: createAlertingServiceProviderMock(), + } as unknown) as jest.Mocked); + +const createStartContract = () => jest.fn(); + +export const mlPluginServerMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 3f0a02f5eaad8..bbfc304958f9a 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -195,12 +195,13 @@ function getTrainingPercentMessage(trainingDocs: number) { async function getValidationCheckMessages( asCurrentUser: IScopedClusterClient['asCurrentUser'], analyzedFields: string[], - index: string | string[], analysisConfig: AnalysisConfig, - query: estypes.QueryContainer = defaultQuery + source: DataFrameAnalyticsConfig['source'] ) { const analysisType = getAnalysisType(analysisConfig); const depVar = getDependentVar(analysisConfig); + const index = source.index; + const query = source.query || defaultQuery; const messages = []; const emptyFields: string[] = []; const percentEmptyLimit = FRACTION_EMPTY_LIMIT * 100; @@ -236,6 +237,7 @@ async function getValidationCheckMessages( size: 0, track_total_hits: true, body: { + ...(source.runtime_mappings ? { runtime_mappings: source.runtime_mappings } : {}), query, aggs, }, @@ -247,21 +249,22 @@ async function getValidationCheckMessages( if (body.aggregations) { // @ts-expect-error Object.entries(body.aggregations).forEach(([aggName, { doc_count: docCount, value }]) => { - const empty = docCount / totalDocs; + if (docCount !== undefined) { + const empty = docCount / totalDocs; + if (docCount > 0 && empty > FRACTION_EMPTY_LIMIT) { + emptyFields.push(aggName); - if (docCount > 0 && empty > FRACTION_EMPTY_LIMIT) { - emptyFields.push(aggName); - - if (aggName === depVar) { - depVarValid = false; - dependentVarWarningMessage.text = i18n.translate( - 'xpack.ml.models.dfaValidation.messages.depVarEmptyWarning', - { - defaultMessage: - 'The dependent variable has at least {percentEmpty}% empty values. It may be unsuitable for analysis.', - values: { percentEmpty: percentEmptyLimit }, - } - ); + if (aggName === depVar) { + depVarValid = false; + dependentVarWarningMessage.text = i18n.translate( + 'xpack.ml.models.dfaValidation.messages.depVarEmptyWarning', + { + defaultMessage: + 'The dependent variable has at least {percentEmpty}% empty values. It may be unsuitable for analysis.', + values: { percentEmpty: percentEmptyLimit }, + } + ); + } } } @@ -374,9 +377,8 @@ export async function validateAnalyticsJob( const messages = await getValidationCheckMessages( client.asCurrentUser, job.analyzed_fields.includes, - job.source.index, job.analysis, - job.source.query + job.source ); return messages; } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 0287c2af11a7e..c6cf608fe1e0b 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -80,7 +80,7 @@ class FieldsService { if (firstKey !== undefined) { const field = fc[firstKey]; // add to the list of fields if the field type can be used by ML - if (supportedTypes.includes(field.type) === true) { + if (supportedTypes.includes(field.type) === true && field.metadata_field !== true) { fields.push({ id: k, name: k, diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json index 599dee1e56c0f..6d01a853698b8 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../../../tsconfig.project.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "target": "es6", diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.ts new file mode 100644 index 0000000000000..957321e61b83a --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/alerting_service.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const createAlertingServiceProviderMock = () => + jest.fn(() => ({ + preview: jest.fn(), + execute: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts new file mode 100644 index 0000000000000..12b12e4ba06df --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/anomaly_detectors.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createAnomalyDetectorsProviderMock = () => + jest.fn(() => ({ + jobs: jest.fn(), + jobStats: jest.fn(), + datafeeds: jest.fn(), + datafeedStats: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.ts new file mode 100644 index 0000000000000..e39373d66eff8 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/jobs_service.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createJobServiceProviderMock = () => + jest.fn(() => ({ + jobsSummary: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts new file mode 100644 index 0000000000000..b33e11dae5879 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/modules.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createModulesProviderMock = () => + jest.fn(() => ({ + recognize: jest.fn(), + getModule: jest.fn(), + listModules: jest.fn(), + setup: jest.fn(), + })); diff --git a/x-pack/plugins/maps_legacy_licensing/public/index.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.ts similarity index 68% rename from x-pack/plugins/maps_legacy_licensing/public/index.ts rename to x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.ts index 9105919eaa635..7fd60d0b3428d 100644 --- a/x-pack/plugins/maps_legacy_licensing/public/index.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/results_service.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { MapsLegacyLicensing } from './plugin'; - -export function plugin() { - return new MapsLegacyLicensing(); -} +export const createResultsServiceProviderMock = () => + jest.fn(() => ({ + getAnomaliesTableData: jest.fn(), + })); diff --git a/x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.ts b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.ts new file mode 100644 index 0000000000000..c002ddc4ced52 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/providers/__mocks__/system.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createMockMlSystemProvider = () => + jest.fn(() => ({ + mlCapabilities: jest.fn(), + mlInfo: jest.fn(), + mlAnomalySearch: jest.fn(), + })); diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index b1135b867dc08..6b396b1c59642 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index b1999101f7c12..d0fb7e1a88dcf 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx index b1ca3c614fc70..fce1cde38f587 100644 --- a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx @@ -5,53 +5,38 @@ * 2.0. */ -import React from 'react'; -import { EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiTitle } from '@elastic/eui'; +import { EuiCard, EuiLink, EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiText } from '@elastic/eui'; -import { EuiLink } from '@elastic/eui'; +import React from 'react'; import { usePluginContext } from '../../../hooks/use_plugin_context'; export function FleetPanel() { const { core } = usePluginContext(); return ( - - - - -

    - {i18n.translate('xpack.observability.fleet.title', { - defaultMessage: 'Have you seen our new Fleet?', - })} -

    -
    -
    - - - {i18n.translate('xpack.observability.fleet.text', { - defaultMessage: - 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', - })} - - - - - {i18n.translate('xpack.observability.fleet.button', { - defaultMessage: 'Try Fleet Beta', - })} - - -
    -
    + description={ + + {i18n.translate('xpack.observability.fleet.text', { + defaultMessage: + 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', + })} + + } + footer={ + + {i18n.translate('xpack.observability.fleet.button', { + defaultMessage: 'Try Fleet Beta', + })} + + } + title={i18n.translate('xpack.observability.fleet.title', { + defaultMessage: 'Have you seen our new Fleet?', + })} + /> ); } 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 index e5f100be285e1..d29481a39eb72 100644 --- 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 @@ -56,6 +56,32 @@ describe('APMSection', () => { } as unknown) as ObservabilityPublicPluginsStart, })); }); + + it('renders transaction stat less then 1k', () => { + const resp = { + appLink: '/app/apm', + stats: { + services: { value: 11, type: 'number' }, + transactions: { value: 900, type: 'number' }, + }, + series: { + transactions: { coordinates: [] }, + }, + }; + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: resp, + 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('Throughput 900.0 tpm')).toBeInTheDocument(); + expect(queryAllByTestId('loading')).toEqual([]); + }); + it('renders with transaction series and stats', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, 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 index 91a536840ecbd..e71468d3b028c 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -31,6 +31,19 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } +function formatTpmStat(value?: number) { + if (!value || value === 0) { + return '0'; + } + if (value <= 0.1) { + return '< 0.1'; + } + if (value > 1000) { + return numeral(value).format('0.00a'); + } + return numeral(value).format('0,0.0'); +} + export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const chartTheme = useChartTheme(); @@ -93,7 +106,7 @@ export function APMSection({ bucketSize }: Props) { + {platformLogo !== null && } +   + {providerLogo !== null && } +   + {name} + + ); +} 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 index dfdede6e7b32f..5a642084733c7 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -5,49 +5,56 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui'; +import { + Criteria, + Direction, + EuiBasicTable, + EuiBasicTableColumn, + EuiTableSortingType, +} 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 React, { useState, useCallback } from 'react'; +import { + MetricsFetchDataResponse, + MetricsFetchDataSeries, + NumberOrNull, + StringOrNull, +} from '../../../..'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { useTimeRange } from '../../../../hooks/use_time_range'; -import { StyledStat } from '../../styled_stat'; +import { HostLink } from './host_link'; +import { formatDuration } from './lib/format_duration'; +import { MetricWithSparkline } from './metric_with_sparkline'; + +const SPARK_LINE_COLUMN_WIDTH = '120px'; +const COLOR_ORANGE = 7; +const COLOR_BLUE = 1; +const COLOR_GREEN = 0; +const COLOR_PURPLE = 3; interface Props { 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}; - } +const percentFormatter = (value: NumberOrNull) => + value === null ? '' : numeral(value).format('0[.0]%'); - &::-moz-progress-bar { - background-color: ${(props) => props.color}; - } - } +const numberFormatter = (value: NumberOrNull) => + value === null ? '' : numeral(value).format('0[.0]'); - &.euiProgress--indeterminate { - &:before { - background-color: ${(props) => props.color}; - } - } - } -`; +const bytesPerSecondFormatter = (value: NumberOrNull) => + value === null ? '' : numeral(value).format('0b') + '/s'; export function MetricsSection({ bucketSize }: Props) { - const theme = useContext(ThemeContext); const { forceUpdate, hasData } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); + const [sortDirection, setSortDirection] = useState('asc'); + const [sortField, setSortField] = useState('uptime'); + const [sortedData, setSortedData] = useState(null); const { data, status } = useFetcher( () => { @@ -64,16 +71,138 @@ export function MetricsSection({ bucketSize }: Props) { [bucketSize, relativeStart, relativeEnd, forceUpdate] ); + const handleTableChange = useCallback( + ({ sort }: Criteria) => { + if (sort) { + const { field, direction } = sort; + setSortField(field); + setSortDirection(direction); + if (data) { + (async () => { + const response = await data.sort(field, direction); + setSortedData(response || null); + })(); + } + } + }, + [data, setSortField, setSortDirection] + ); + if (!hasData.infra_metrics?.hasData) { return null; } const isLoading = status === FETCH_STATUS.LOADING; + const isPending = status === FETCH_STATUS.LOADING; + if (isLoading || isPending) { + return
    Loading
    ; + } + + if (!data) { + return
    No Data
    ; + } + + const columns: Array> = [ + { + field: 'uptime', + name: i18n.translate('xpack.observability.overview.metrics.colunms.uptime', { + defaultMessage: 'Uptime', + }), + sortable: true, + width: '80px', + render: (value: NumberOrNull) => (value == null ? 'N/A' : formatDuration(value / 1000)), + }, + { + field: 'name', + name: i18n.translate('xpack.observability.overview.metrics.colunms.hostname', { + defaultMessage: 'Hostname', + }), + sortable: true, + truncateText: true, + isExpander: true, + render: (value: StringOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'cpu', + name: i18n.translate('xpack.observability.overview.metrics.colunms.cpu', { + defaultMessage: 'CPU %', + }), + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'load', + name: i18n.translate('xpack.observability.overview.metrics.colunms.load15', { + defaultMessage: 'Load 15', + }), + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'rx', + name: 'RX', + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'tx', + name: 'TX', + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + ]; + + const sorting: EuiTableSortingType = { + sort: { field: sortField, direction: sortDirection }, + }; - const { appLink, stats } = data || {}; + const viewData = sortedData || data; - const cpuColor = theme.eui.euiColorVis7; - const memoryColor = theme.eui.euiColorVis0; + const { appLink } = data || {}; return ( - - - - - - - - - - - - - - - - - - - - - + ); } diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts new file mode 100644 index 0000000000000..b4b03b2194ef2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { formatDuration } from './format_duration'; + +describe('formatDuration(seconds)', () => { + it('should work for less then a minute', () => { + expect(formatDuration(56)).toBe('56s'); + }); + + it('should work for less then a hour', () => { + expect(formatDuration(2000)).toBe('33m 20s'); + }); + + it('should work for less then a day', () => { + expect(formatDuration(74566)).toBe('20h 42m'); + }); + + it('should work for more then a day', () => { + expect(formatDuration(86400 * 3 + 3600 * 4)).toBe('3d 4h'); + expect(formatDuration(86400 * 419 + 3600 * 6)).toBe('419d 6h'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts new file mode 100644 index 0000000000000..29fb1dcbd1b52 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const MINUTE = 60; +const HOUR = 3600; +const DAY = 86400; + +export const formatDuration = (seconds: number) => { + if (seconds < MINUTE) { + return `${Math.floor(seconds)}s`; + } + if (seconds < HOUR) { + return `${Math.floor(seconds / MINUTE)}m ${Math.floor(seconds % MINUTE)}s`; + } + if (seconds < DAY) { + return `${Math.floor(seconds / HOUR)}h ${Math.floor((seconds % HOUR) / MINUTE)}m`; + } + return `${Math.floor(seconds / DAY)}d ${Math.floor((seconds % DAY) / HOUR)}h`; +}; diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg new file mode 100644 index 0000000000000..6d26c99bec674 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg @@ -0,0 +1,83 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg new file mode 100644 index 0000000000000..f53491803db44 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg new file mode 100644 index 0000000000000..73630c9ba2630 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg new file mode 100644 index 0000000000000..4f026bb4dbac5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg new file mode 100644 index 0000000000000..4516c4e302ba4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg new file mode 100644 index 0000000000000..c9ab6aed30151 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg new file mode 100644 index 0000000000000..c0a92e0c0f404 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg @@ -0,0 +1,1532 @@ + + + + Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Tux + 20 June 2012 + + + Garrett LeSage + + + + + + Larry Ewing, the creator of the original Tux graphic + + + + + tux + Linux + penguin + logo + + + + + Larry Ewing, Garrett LeSage + + + https://github.com/garrett/Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg new file mode 100644 index 0000000000000..7cc046187eb60 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg new file mode 100644 index 0000000000000..1a211689f86f3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg @@ -0,0 +1,54 @@ + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Open Icon Library + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx new file mode 100644 index 0000000000000..3cb61f85d57f0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Chart, Settings, AreaSeries } from '@elastic/charts'; +import { EuiIcon, EuiTextColor } from '@elastic/eui'; +import React, { useContext } from 'react'; +import { + EUI_CHARTS_THEME_DARK, + EUI_CHARTS_THEME_LIGHT, + EUI_SPARKLINE_THEME_PARTIAL, +} from '@elastic/eui/dist/eui_charts_theme'; +import { ThemeContext } from 'styled-components'; + +import { NumberOrNull } from '../../../..'; + +interface Props { + id: string; + value: NumberOrNull; + timeseries: any[]; + formatter: (value: NumberOrNull) => string; + color: number; +} +export function MetricWithSparkline({ id, formatter, value, timeseries, color }: Props) { + const themeCTX = useContext(ThemeContext); + const isDarkTheme = (themeCTX && themeCTX.darkMode) || false; + const theme = [ + EUI_SPARKLINE_THEME_PARTIAL, + isDarkTheme ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, + ]; + + const colors = theme[1].colors?.vizColors ?? []; + + if (!value) { + return ( + + +  N/A + + ); + } + return ( + <> + + + + +   + {formatter(value)} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts index a31679c61a4ab..3fcf98f712bef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { OperationType } from '../../../../../../lens/public'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; +import { OperationType } from '../../../../../../../lens/public'; export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -20,7 +20,7 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'transaction.duration.us', label: 'Latency', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts index 32cae2167ddf0..c0f3d6dc9b010 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { OperationType } from '../../../../../../lens/public'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants/constants'; +import { buildPhraseFilter } from '../utils'; +import { OperationType } from '../../../../../../../lens/public'; export function getServiceThroughputLensConfig({ seriesId, @@ -23,7 +23,7 @@ export function getServiceThroughputLensConfig({ sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'transaction.duration.us', label: 'Throughput', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts similarity index 80% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index aa3ac2fa64317..ed849c1eb47b3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,14 +5,8 @@ * 2.0. */ -import { AppDataType, ReportViewTypeId } from '../types'; -import { - CLS_FIELD, - FCP_FIELD, - FID_FIELD, - LCP_FIELD, - TBT_FIELD, -} from './data/elasticsearch_fieldnames'; +import { AppDataType, ReportViewTypeId } from '../../types'; +import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; export const FieldLabels: Record = { 'user_agent.name': 'Browser family', @@ -24,10 +18,10 @@ export const FieldLabels: Record = { 'service.name': 'Service Name', 'service.environment': 'Environment', - [LCP_FIELD]: 'Largest contentful paint', - [FCP_FIELD]: 'First contentful paint', - [TBT_FIELD]: 'Total blocking time', - [FID_FIELD]: 'First input delay', + [LCP_FIELD]: 'Largest contentful paint (Seconds)', + [FCP_FIELD]: 'First contentful paint (Seconds)', + [TBT_FIELD]: 'Total blocking time (Seconds)', + [FID_FIELD]: 'First input delay (Seconds)', [CLS_FIELD]: 'Cumulative layout shift', 'monitor.id': 'Monitor Id', @@ -38,6 +32,7 @@ export const FieldLabels: Record = { 'monitor.name': 'Monitor name', 'monitor.type': 'Monitor Type', 'url.port': 'Port', + 'url.full': 'Url', tags: 'Tags', // custom diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts similarity index 86% rename from x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts index c41b3a2bf5eb5..63661f0d5a996 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { Typeahead } from './typehead'; +export * from './constants'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 85d48ef638d44..2c5b4ebea0ab3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -6,16 +6,16 @@ */ import { ReportViewTypes } from '../types'; -import { getPerformanceDistLensConfig } from './performance_dist_config'; -import { getMonitorDurationConfig } from './monitor_duration_config'; -import { getServiceLatencyLensConfig } from './service_latency_config'; -import { getMonitorPingsConfig } from './monitor_pings_config'; -import { getServiceThroughputLensConfig } from './service_throughput_config'; -import { getKPITrendsLensConfig } from './kpi_trends_config'; -import { getCPUUsageLensConfig } from './cpu_usage_config'; -import { getMemoryUsageLensConfig } from './memory_usage_config'; -import { getNetworkActivityLensConfig } from './network_activity_config'; -import { getLogsFrequencyLensConfig } from './logs_frequency_config'; +import { getPerformanceDistLensConfig } from './rum/performance_dist_config'; +import { getMonitorDurationConfig } from './synthetics/monitor_duration_config'; +import { getServiceLatencyLensConfig } from './apm/service_latency_config'; +import { getMonitorPingsConfig } from './synthetics/monitor_pings_config'; +import { getServiceThroughputLensConfig } from './apm/service_throughput_config'; +import { getKPITrendsLensConfig } from './rum/kpi_trends_config'; +import { getCPUUsageLensConfig } from './metrics/cpu_usage_config'; +import { getMemoryUsageLensConfig } from './metrics/memory_usage_config'; +import { getNetworkActivityLensConfig } from './metrics/network_activity_config'; +import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index dcfaed938cc0f..139f3ab0d82ed 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -8,9 +8,8 @@ import { LensAttributes } from './lens_attributes'; import { mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; -import { sampleAttribute } from './data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames'; -import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames'; +import { sampleAttribute } from './test_data/sample_attribute'; +import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; describe('Lens Attribute', () => { const reportViewConfig = getDefaultConfigs({ @@ -93,7 +92,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -129,7 +128,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getXAxis()).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -154,7 +153,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -318,7 +317,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -363,7 +362,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts index 68e5e697d2f9d..8a27d7ddd428b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts index 5a4fb2aa3a6a5..6214975d8f1dd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'system.cpu.user.pct', label: 'CPU Usage %', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts index 579372ed86fa7..6f46c175f7882 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'system.memory.used.pct', label: 'Memory Usage %', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts similarity index 81% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts index 63cdd0ec8bd60..1bc9fed9c3f80 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'system.memory.used.pct', }, hasMetricType: true, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts new file mode 100644 index 0000000000000..f1fc5f310b8ef --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldFormat } from '../../types'; +import { + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_DURATION, +} from '../constants/elasticsearch_fieldnames'; + +export const rumFieldFormats: FieldFormat[] = [ + { + field: TRANSACTION_DURATION, + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asSeconds', + showSuffix: true, + outputPrecision: 1, + }, + }, + }, + { + field: FCP_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: LCP_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: TBT_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: FID_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts index a967a8824bca7..a1a3acd51f89c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, PROCESSOR_EVENT, @@ -18,7 +18,7 @@ import { USER_AGENT_NAME, USER_AGENT_OS, USER_AGENT_VERSION, -} from './data/elasticsearch_fieldnames'; +} from '../constants/elasticsearch_fieldnames'; export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts index 41617304c9f3d..7005dea29d60d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, CLS_FIELD, @@ -24,7 +24,7 @@ import { USER_AGENT_NAME, USER_AGENT_OS, USER_AGENT_VERSION, -} from './data/elasticsearch_fieldnames'; +} from '../constants/elasticsearch_fieldnames'; export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -80,7 +80,7 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application', - [TRANSACTION_DURATION]: 'Page load time', + [TRANSACTION_DURATION]: 'Page load time (Seconds)', }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts new file mode 100644 index 0000000000000..4f036f0b9be65 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldFormat } from '../../types'; + +export const syntheticsFieldFormats: FieldFormat[] = [ + { + field: 'monitor.duration.us', + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asMilliseconds', + outputPrecision: 0, + }, + }, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts similarity index 83% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts index aa9b8b94c6d86..f0ec3f0c31bef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants/constants'; +import { OperationType } from '../../../../../../../lens/public'; interface Props { seriesId: string; @@ -23,7 +23,7 @@ export function getMonitorDurationConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average' as OperationType, sourceField: 'monitor.duration.us', label: 'Monitor duration (ms)', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts index 72968626e934b..40c9f5750fb4d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts similarity index 98% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 9b299e7d70bcc..ffce81207472f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -21,7 +21,7 @@ export const sampleAttribute = { columns: { 'x-axis-column': { sourceField: 'transaction.duration.us', - label: 'Page load time', + label: 'Page load time (Seconds)', dataType: 'number', operationType: 'range', isBucketed: true, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 38b8ce81b2acd..c885673134786 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,11 +5,11 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage'; +import type { AllSeries, AllShortSeries } from '../hooks/use_url_storage'; import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { URL_KEYS } from './url_constants'; +import { URL_KEYS } from './constants/url_constants'; export function convertToShortUrl(series: SeriesUrl) { const { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 7e99874f557b3..257eb3a739f0f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -6,12 +6,11 @@ */ import React from 'react'; -import { within } from '@testing-library/react'; import { fireEvent, screen, waitFor } from '@testing-library/dom'; import { render, mockUrlStorage, mockCore } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; -import * as obsvInd from '../../../utils/observability_index_patterns'; +import * as obsvInd from './utils/observability_index_patterns'; describe('ExploratoryView', () => { beforeEach(() => { @@ -56,10 +55,10 @@ describe('ExploratoryView', () => { await waitFor(() => { screen.getByText(/open in lens/i); + }); + + await waitFor(() => { screen.getByText(/select a data type to start building a series\./i); - screen.getByRole('table', { name: /this table contains 1 rows\./i }); - const button = screen.getByRole('button', { name: /add/i }); - within(button).getByText(/add/i); }); await fireEvent.click(screen.getByText(/cancel/i)); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index b3ad107bbe0e2..0e7bc80e8659c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; import { SeriesEditor } from './series_editor/series_editor'; -import { useUrlStorage } from './hooks/use_url_strorage'; +import { useUrlStorage } from './hooks/use_url_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { EmptyView } from './components/empty_view'; import { useIndexPatternContext } from './hooks/use_default_index_pattern'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index bda3566c76602..17f06436c8535 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -12,7 +12,7 @@ import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { DataViewLabels } from '../configurations/constants'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx index 04cbb4a4ddb18..7ead7d5e3cfad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx @@ -10,7 +10,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { AppDataType } from '../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; -import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns'; +import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; export interface IIndexPatternContext { indexPattern: IndexPattern; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts index 9f462790e8d37..76fd64ef86736 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts @@ -8,12 +8,9 @@ import { useFetcher } from '../../../..'; import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; -import { AllShortSeries } from './use_url_strorage'; +import { AllShortSeries } from './use_url_storage'; import { ReportToDataTypeMap } from '../configurations/constants'; -import { - DataType, - ObservabilityIndexPatterns, -} from '../../../../utils/observability_index_patterns'; +import { DataType, ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { const { @@ -30,7 +27,7 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { const firstSeries = allSeries[firstSeriesId]; - const { data: indexPattern } = useFetcher(() => { + const { data: indexPattern, error } = useFetcher(() => { const obsvIndexP = new ObservabilityIndexPatterns(data); let reportType: DataType = 'apm'; if (firstSeries?.rt) { @@ -40,5 +37,9 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { return obsvIndexP.getIndexPattern(reportType); }, [firstSeries?.rt, data]); + if (error) { + throw error; + } + return indexPattern; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c735009f66f9..274542380c137 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,7 +8,7 @@ import { useMemo } from 'react'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LensAttributes } from '../configurations/lens_attributes'; -import { useUrlStorage } from './use_url_strorage'; +import { useUrlStorage } from './use_url_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index 35247180c2ee5..34f0a7c1a7f86 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useUrlStorage } from './use_url_strorage'; +import { useUrlStorage } from './use_url_storage'; import { UrlFilter } from '../types'; export interface UpdateFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx similarity index 97% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx index d38429703b709..6256b3b134f8c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx @@ -10,7 +10,7 @@ import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_ import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types'; import { convertToShortUrl } from '../configurations/utils'; import { OperationType, SeriesType } from '../../../../../../lens/public'; -import { URL_KEYS } from '../configurations/url_constants'; +import { URL_KEYS } from '../configurations/constants/url_constants'; export const UrlStorageContext = createContext(null); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index dc47a0f075fe6..f903c4d7d44fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -18,7 +18,7 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../../../src/plugins/kibana_utils/public/'; -import { UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { UrlStorageContextProvider } from './hooks/use_url_storage'; import { useInitExploratoryView } from './hooks/use_init_exploratory_view'; import { WithHeaderLayout } from '../../app/layout/with_header'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 112bfcc3ccb58..b826409dd9e3a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -23,20 +23,20 @@ import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { lensPluginMock } from '../../../../../lens/public/mocks'; import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; -import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_storage'; import { withNotifyOnErrors, createKbnUrlStateStorage, } from '../../../../../../../src/plugins/kibana_utils/public'; import * as fetcherHook from '../../../hooks/use_fetcher'; -import * as useUrlHook from './hooks/use_url_strorage'; +import * as useUrlHook from './hooks/use_url_storage'; import * as useSeriesFilterHook from './hooks/use_series_filters'; import * as useHasDataHook from '../../../hooks/use_has_data'; import * as useValuesListHook from '../../../hooks/use_values_list'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub'; -import indexPatternData from './configurations/data/test_index_pattern.json'; +import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index d33d8515d3bee..039cdfc9b73f5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { mockUrlStorage, render } from '../../rtl_helpers'; import { dataTypes, DataTypesCol } from './data_types_col'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; describe('DataTypesCol', function () { it('should render properly', function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 7ea44e66a721a..b6464bbe3c6ed 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AppDataType } from '../../types'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; export const dataTypes: Array<{ id: AppDataType; label: string }> = [ { id: 'synthetics', label: 'Synthetic Monitoring' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index dba660fff9c36..553aff57ad491 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -10,9 +10,9 @@ import { fireEvent, screen } from '@testing-library/react'; import { render } from '../../../../../utils/test_helper'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { ReportBreakdowns } from './report_breakdowns'; -import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportBreakdowns', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx index 7667cea417a52..619e2ec4fe9b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { DataSeries } from '../../types'; export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 2fda581154166..104a8fcefb49f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { ReportDefinitionCol } from './report_definition_col'; -import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index ce11c869de0ab..b907efb57d5c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; import { CustomReportField } from '../custom_report_field'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { DataSeries } from '../../types'; @@ -67,6 +67,7 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe {rtd?.[field] && ( ; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx index 6039fd4cba280..4d5033eca241b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; import { ReportDefinition } from '../types'; interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index 983c18af031d0..053f301529635 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -16,7 +16,7 @@ import { ReportTypesCol } from './columns/report_types_col'; import { ReportDefinitionCol } from './columns/report_definition_col'; import { ReportFilters } from './columns/report_filters'; import { ReportBreakdowns } from './columns/report_breakdowns'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx index 71e3317ad6db8..922d33ffd39ac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -8,7 +8,7 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHasData } from '../../../../hooks/use_has_data'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; export interface TimePickerTime { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 654a93a08a7c8..0824f13e6b3fe 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { getDefaultConfigs } from '../../configurations/default_configs'; -import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Breakdowns', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 0d34d7245725a..5561779daa8c4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldLabels } from '../../configurations/constants'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx index 017655053eef2..f83630cff414a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx @@ -19,7 +19,7 @@ import styled from 'styled-components'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; import { useFetcher } from '../../../../..'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { SeriesType } from '../../../../../../../lens/public'; export function SeriesChartTypes({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index edd5546f13940..530b8dee3a4d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; -import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 280912dd0902f..3e6d7890f4c81 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -14,7 +14,7 @@ import { EuiFilterGroup, } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 7f76c9ea999ee..befbb3b74d6d7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -12,7 +12,7 @@ import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from ' import { USER_AGENT_NAME, USER_AGENT_VERSION, -} from '../../configurations/data/elasticsearch_fieldnames'; +} from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterValueButton', function () { it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 42cdfd595e66b..efccb351c2619 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx index e01e371b5eeeb..fa4202d2c30ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx @@ -8,12 +8,12 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { OperationType } from '../../../../../../../lens/public'; const toggleButtons = [ { - id: `avg`, + id: `average`, label: i18n.translate('xpack.observability.expView.metricsSelect.average', { defaultMessage: 'Average', }), @@ -49,7 +49,7 @@ export function MetricSelection({ const [isOpen, setIsOpen] = useState(false); - const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg'); + const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'average'); const onChange = (optionId: OperationType) => { setToggleIdSelected(optionId); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx index 67aebed943326..aaaa02c7c5697 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { DataSeries } from '../../types'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; interface Props { series: DataSeries; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index 24b65d2adb38e..c9bb44cfd8cca 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -17,9 +17,9 @@ import { } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; import { DataSeries } from '../../types'; -import { FieldLabels } from '../../configurations/constants'; +import { FieldLabels } from '../../configurations/constants/constants'; import { SelectedFilters } from '../selected_filters'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 5770a7e209f06..a38b50d610c75 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -10,8 +10,8 @@ import { screen, waitFor } from '@testing-library/react'; import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { NEW_SERIES_KEY } from '../hooks/use_url_strorage'; -import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames'; +import { NEW_SERIES_KEY } from '../hooks/use_url_storage'; +import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; describe('SelectedFilters', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx index be8b1feb4d723..34e69f688eaaf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { FilterLabel } from '../components/filter_label'; import { DataSeries, UrlFilter } from '../types'; import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 2d423c9aee3fc..2d8bd12904fbd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -13,7 +13,7 @@ import { ActionsCol } from './columns/actions_col'; import { Breakdowns } from './columns/breakdowns'; import { DataSeries } from '../types'; import { SeriesBuilder } from '../series_builder/series_builder'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DatePickerCol } from './columns/date_picker_col'; import { RemoveSeries } from './columns/remove_series'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 444e0ddaecb4a..d673fc4d6f6ee 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -87,3 +87,22 @@ export interface ConfigProps { } export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm'; + +type FormatType = 'duration' | 'number'; +type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; +type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize'; + +export interface FieldFormatParams { + inputFormat: InputFormat; + outputFormat: OutputFormat; + outputPrecision?: number; + showSuffix?: boolean; +} + +export interface FieldFormat { + field: string; + format: { + id: FormatType; + params: FieldFormatParams; + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts new file mode 100644 index 0000000000000..b6f544db2a319 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { indexPatternList, ObservabilityIndexPatterns } from './observability_index_patterns'; +import { mockCore, mockIndexPattern } from '../rtl_helpers'; +import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public'; + +const fieldFormats = { + 'transaction.duration.us': { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asSeconds', + outputPrecision: 1, + showSuffix: true, + }, + }, + 'transaction.experience.fid': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.experience.tbt': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.marks.agent.firstContentfulPaint': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.marks.agent.largestContentfulPaint': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, +}; + +describe('ObservabilityIndexPatterns', function () { + const { data } = mockCore(); + data!.indexPatterns.get = jest.fn().mockReturnValue({ title: 'index-*' }); + data!.indexPatterns.createAndSave = jest.fn().mockReturnValue({ id: indexPatternList.rum }); + data!.indexPatterns.updateSavedObject = jest.fn(); + + it('should return index pattern for app', async function () { + const obsv = new ObservabilityIndexPatterns(data!); + + const indexP = await obsv.getIndexPattern('rum'); + + expect(indexP).toEqual({ title: 'index-*' }); + + expect(data?.indexPatterns.get).toHaveBeenCalledWith(indexPatternList.rum); + expect(data?.indexPatterns.get).toHaveBeenCalledTimes(1); + }); + + it('should creates missing index pattern', async function () { + data!.indexPatterns.get = jest.fn().mockImplementation(() => { + throw new SavedObjectNotFound('index_pattern'); + }); + + const obsv = new ObservabilityIndexPatterns(data!); + + const indexP = await obsv.getIndexPattern('rum'); + + expect(indexP).toEqual({ id: indexPatternList.rum }); + + expect(data?.indexPatterns.createAndSave).toHaveBeenCalledWith({ + fieldFormats, + id: 'rum_static_index_pattern_id', + timeFieldName: '@timestamp', + title: '(rum-data-view)*,apm-*', + }); + expect(data?.indexPatterns.createAndSave).toHaveBeenCalledTimes(1); + }); + + it('should return getFieldFormats', function () { + const obsv = new ObservabilityIndexPatterns(data!); + + expect(obsv.getFieldFormats('rum')).toEqual(fieldFormats); + }); + + it('should validate field formats', async function () { + mockIndexPattern.getFormatterForField = jest.fn().mockReturnValue({ params: () => {} }); + + const obsv = new ObservabilityIndexPatterns(data!); + + await obsv.validateFieldFormats('rum', mockIndexPattern); + + expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledTimes(1); + expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledWith( + expect.objectContaining({ fieldFormatMap: fieldFormats }) + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts new file mode 100644 index 0000000000000..e0a2941b24d3c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { + DataPublicPluginStart, + IndexPattern, + FieldFormat as IFieldFormat, + IndexPatternSpec, +} from '../../../../../../../../src/plugins/data/public'; +import { rumFieldFormats } from '../configurations/rum/field_formats'; +import { syntheticsFieldFormats } from '../configurations/synthetics/field_formats'; +import { FieldFormat, FieldFormatParams } from '../types'; + +const appFieldFormats: Record = { + rum: rumFieldFormats, + apm: null, + logs: null, + metrics: null, + synthetics: syntheticsFieldFormats, +}; + +function getFieldFormatsForApp(app: DataType) { + return appFieldFormats[app]; +} + +export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum'; + +export const indexPatternList: Record = { + synthetics: 'synthetics_static_index_pattern_id', + apm: 'apm_static_index_pattern_id', + rum: 'rum_static_index_pattern_id', + logs: 'logs_static_index_pattern_id', + metrics: 'metrics_static_index_pattern_id', +}; + +const appToPatternMap: Record = { + synthetics: '(synthetics-data-view)*,heartbeat-*,synthetics-*', + apm: 'apm-*', + rum: '(rum-data-view)*,apm-*', + logs: 'logs-*,filebeat-*', + metrics: 'metrics-*,metricbeat-*', +}; + +export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) { + return ( + param1?.inputFormat === param2?.inputFormat && + param1?.outputFormat === param2?.outputFormat && + param1?.showSuffix === param2?.showSuffix && + param2?.outputPrecision === param1?.outputPrecision + ); +} + +export class ObservabilityIndexPatterns { + data?: DataPublicPluginStart; + + constructor(data: DataPublicPluginStart) { + this.data = data; + } + + async createIndexPattern(app: DataType) { + if (!this.data) { + throw new Error('data is not defined'); + } + + const pattern = appToPatternMap[app]; + + return await this.data.indexPatterns.createAndSave({ + title: pattern, + id: indexPatternList[app], + timeFieldName: '@timestamp', + fieldFormats: this.getFieldFormats(app), + }); + } + // we want to make sure field formats remain same + async validateFieldFormats(app: DataType, indexPattern: IndexPattern) { + const defaultFieldFormats = getFieldFormatsForApp(app); + if (defaultFieldFormats && defaultFieldFormats.length > 0) { + let isParamsDifferent = false; + defaultFieldFormats.forEach(({ field, format }) => { + const fieldFormat = indexPattern.getFormatterForField(indexPattern.getFieldByName(field)!); + const params = fieldFormat.params(); + if (!isParamsSame(params, format.params)) { + indexPattern.setFieldFormat(field, format); + isParamsDifferent = true; + } + }); + if (isParamsDifferent) { + await this.data?.indexPatterns.updateSavedObject(indexPattern); + } + } + } + + getFieldFormats(app: DataType) { + const fieldFormatMap: IndexPatternSpec['fieldFormats'] = {}; + + (appFieldFormats?.[app] ?? []).forEach(({ field, format }) => { + fieldFormatMap[field] = format; + }); + + return fieldFormatMap; + } + + async getIndexPattern(app: DataType): Promise { + if (!this.data) { + throw new Error('data is not defined'); + } + try { + const indexPattern = await this.data?.indexPatterns.get(indexPatternList[app]); + + // this is intentional a non blocking call, so no await clause + this.validateFieldFormats(app, indexPattern); + return indexPattern; + } catch (e: unknown) { + if (e instanceof SavedObjectNotFound) { + return await this.createIndexPattern(app || 'apm'); + } + } + } +} diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index a44aab2da85be..d14039ba173ac 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -76,6 +76,7 @@ export function FieldValueSelection({ { }); describe('Metrics', () => { + const makeRequestResponse = { + title: 'metrics', + appLink: '/metrics', + sort: () => makeRequest(), + series: [], + }; + const makeRequest = async () => { + return makeRequestResponse; + }; registerDataHandler({ appName: 'infra_metrics', - fetchData: async () => { - return { - title: 'metrics', - appLink: '/metrics', - stats: { - hosts: { - label: 'hosts', - type: 'number', - value: 1, - }, - cpu: { - label: 'cpu', - type: 'number', - value: 1, - }, - memory: { - label: 'memory', - type: 'number', - value: 1, - }, - disk: { - label: 'disk', - type: 'number', - value: 1, - }, - inboundTraffic: { - label: 'inboundTraffic', - type: 'number', - value: 1, - }, - outboundTraffic: { - label: 'outboundTraffic', - type: 'number', - value: 1, - }, - }, - series: { - inboundTraffic: { - label: 'inbound Traffic', - coordinates: [{ x: 1 }], - }, - outboundTraffic: { - label: 'outbound Traffic', - coordinates: [{ x: 1 }], - }, - }, - }; - }, + fetchData: makeRequest, hasData: async () => true, }); @@ -383,52 +345,7 @@ describe('registerDataHandler', () => { it('returns data when fetchData is called', async () => { const dataHandler = getDataHandler('infra_metrics'); const response = await dataHandler?.fetchData(params); - expect(response).toEqual({ - title: 'metrics', - appLink: '/metrics', - stats: { - hosts: { - label: 'hosts', - type: 'number', - value: 1, - }, - cpu: { - label: 'cpu', - type: 'number', - value: 1, - }, - memory: { - label: 'memory', - type: 'number', - value: 1, - }, - disk: { - label: 'disk', - type: 'number', - value: 1, - }, - inboundTraffic: { - label: 'inboundTraffic', - type: 'number', - value: 1, - }, - outboundTraffic: { - label: 'outboundTraffic', - type: 'number', - value: 1, - }, - }, - series: { - inboundTraffic: { - label: 'inbound Traffic', - coordinates: [{ x: 1 }], - }, - outboundTraffic: { - label: 'outbound Traffic', - coordinates: [{ x: 1 }], - }, - }, - }); + expect(response).toEqual(makeRequestResponse); }); it('returns true when hasData is called', async () => { diff --git a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx new file mode 100644 index 0000000000000..86922b045c742 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/landing/landing.stories.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ComponentType } from 'react'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; +import { PluginContext, PluginContextValue } from '../../context/plugin_context'; +import { LandingPage } from './'; + +export default { + title: 'app/Landing', + component: LandingPage, + decorators: [ + (Story: ComponentType) => { + const pluginContextValue = ({ + appMountParameters: { setHeaderActionMenu: () => {} }, + core: { + http: { + basePath: { + prepend: () => '', + }, + }, + }, + } as unknown) as PluginContextValue; + return ( + + + + + + ); + }, + ], +}; + +export function Example() { + return ; +} 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 index 82f804ba1a938..f88b89e75389e 100644 --- a/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts +++ b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts @@ -12,19 +12,13 @@ export const fetchMetricsData: FetchData = () => { }; const response: MetricsFetchDataResponse = { - appLink: '/app/apm', - stats: { - hosts: { value: 11, type: 'number' }, - cpu: { value: 0.8, type: 'percent' }, - memory: { value: 0.362, type: 'percent' }, - }, + appLink: '/app/metrics', + sort: async () => response, + series: [], }; export const emptyResponse: MetricsFetchDataResponse = { - appLink: '/app/apm', - stats: { - hosts: { value: 0, type: 'number' }, - cpu: { value: 0, type: 'percent' }, - memory: { value: 0, type: 'percent' }, - }, + appLink: '/app/metrics', + sort: async () => emptyResponse, + series: [], }; 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 f4e824784fe09..726c83d0c2256 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 @@ -7,7 +7,6 @@ import { ObservabilityApp } from '../../../typings/common'; import { UXMetrics } from '../../components/shared/core_web_vitals'; - export interface Stat { type: 'number' | 'percent' | 'bytesPerSecond'; value: number; @@ -15,7 +14,7 @@ export interface Stat { export interface Coordinates { x: number; - y?: number; + y?: number | null; } export interface Series { @@ -67,12 +66,33 @@ export interface LogsFetchDataResponse extends FetchDataResponse { series: Record; } +export type StringOrNull = string | null; +export type NumberOrNull = number | null; + +export interface MetricsFetchDataSeries { + id: string; + name: StringOrNull; + platform: StringOrNull; + provider: StringOrNull; + cpu: NumberOrNull; + iowait: NumberOrNull; + load: NumberOrNull; + uptime: NumberOrNull; + rx: NumberOrNull; + tx: NumberOrNull; + timeseries: Array<{ + timestamp: number; + cpu: NumberOrNull; + iowait: NumberOrNull; + load: NumberOrNull; + rx: NumberOrNull; + tx: NumberOrNull; + }>; +} + export interface MetricsFetchDataResponse extends FetchDataResponse { - stats: { - hosts: Stat; - cpu: Stat; - memory: Stat; - }; + sort: (by: string, direction: string) => Promise; + series: MetricsFetchDataSeries[]; } export interface UptimeFetchDataResponse extends FetchDataResponse { diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index cc6e298795e4a..f55ae640a8026 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 03c9e451f3b52..291b0f7c607cf 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/painless_lab/tsconfig.json b/x-pack/plugins/painless_lab/tsconfig.json index 2519206b0fcdb..a869b21e06d4d 100644 --- a/x-pack/plugins/painless_lab/tsconfig.json +++ b/x-pack/plugins/painless_lab/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js deleted file mode 100644 index 38672b4d59a20..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js +++ /dev/null @@ -1,43 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { registerTestBed } from '@kbn/test/jest'; - -import { RemoteClusterAdd } from '../../../public/application/sections/remote_cluster_add'; -import { createRemoteClustersStore } from '../../../public/application/store'; -import { registerRouter } from '../../../public/application/services/routing'; - -const testBedConfig = { - store: createRemoteClustersStore, - memoryRouter: { - onRouter: (router) => registerRouter(router), - }, -}; - -const initTestBed = registerTestBed(RemoteClusterAdd, testBedConfig); - -export const setup = (props) => { - const testBed = initTestBed(props); - - // User actions - const clickSaveForm = async () => { - await act(async () => { - testBed.find('remoteClusterFormSaveButton').simulate('click'); - }); - - testBed.component.update(); - }; - - return { - ...testBed, - actions: { - clickSaveForm, - }, - }; -}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx new file mode 100644 index 0000000000000..a47e6c023a161 --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { registerTestBed } from '@kbn/test/jest'; + +import { RemoteClusterAdd } from '../../../public/application/sections'; +import { createRemoteClustersStore } from '../../../public/application/store'; +import { AppRouter, registerRouter } from '../../../public/application/services'; +import { createRemoteClustersActions } from '../helpers'; +import { AppContextProvider } from '../../../public/application/app_context'; + +const ComponentWithContext = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => { + return ( + + + + ); +}; + +const testBedConfig = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => { + return { + store: createRemoteClustersStore, + memoryRouter: { + onRouter: (router: AppRouter) => registerRouter(router), + }, + defaultProps: { isCloudEnabled }, + }; +}; + +const initTestBed = (isCloudEnabled: boolean) => + registerTestBed(ComponentWithContext, testBedConfig({ isCloudEnabled }))(); + +export const setup = async (isCloudEnabled = false) => { + const testBed = await initTestBed(isCloudEnabled); + + return { + ...testBed, + actions: { + ...createRemoteClustersActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js deleted file mode 100644 index 40abde35835f0..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js +++ /dev/null @@ -1,230 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { setupEnvironment } from '../helpers'; -import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters'; -import { setup } from './remote_clusters_add.helpers'; - -describe('Create Remote cluster', () => { - describe('on component mount', () => { - let find; - let exists; - let actions; - let form; - let server; - let component; - - beforeAll(() => { - ({ server } = setupEnvironment()); - }); - - afterAll(() => { - server.restore(); - }); - - beforeEach(async () => { - await act(async () => { - ({ form, exists, find, actions, component } = setup()); - }); - component.update(); - }); - - test('should have the title of the page set correctly', () => { - expect(exists('remoteClusterPageTitle')).toBe(true); - expect(find('remoteClusterPageTitle').text()).toEqual('Add remote cluster'); - }); - - test('should have a link to the documentation', () => { - expect(exists('remoteClusterDocsButton')).toBe(true); - }); - - test('should have a toggle to Skip unavailable remote cluster', () => { - expect(exists('remoteClusterFormSkipUnavailableFormToggle')).toBe(true); - - // By default it should be set to "false" - expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe( - false - ); - - act(() => { - form.toggleEuiSwitch('remoteClusterFormSkipUnavailableFormToggle'); - }); - - component.update(); - - expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(true); - }); - - test('should have a toggle to enable "proxy" mode for a remote cluster', () => { - expect(exists('remoteClusterFormConnectionModeToggle')).toBe(true); - - // By default it should be set to "false" - expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(false); - - act(() => { - form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); - }); - - component.update(); - - expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(true); - }); - - test('should display errors and disable the save button when clicking "save" without filling the form', async () => { - expect(exists('remoteClusterFormGlobalError')).toBe(false); - expect(find('remoteClusterFormSaveButton').props().disabled).toBe(false); - - await actions.clickSaveForm(); - - expect(exists('remoteClusterFormGlobalError')).toBe(true); - expect(form.getErrorsMessages()).toEqual([ - 'Name is required.', - 'At least one seed node is required.', - ]); - expect(find('remoteClusterFormSaveButton').props().disabled).toBe(true); - }); - }); - - describe('form validation', () => { - describe('remote cluster name', () => { - let component; - let actions; - let form; - - beforeEach(async () => { - await act(async () => { - ({ component, form, actions } = setup()); - }); - - component.update(); - }); - - test('should not allow spaces', async () => { - form.setInputValue('remoteClusterFormNameInput', 'with space'); - - await actions.clickSaveForm(); - - expect(form.getErrorsMessages()).toContain('Spaces are not allowed in the name.'); - }); - - test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => { - const expectInvalidChar = (char) => { - if (char === '-' || char === '_') { - return; - } - - try { - form.setInputValue('remoteClusterFormNameInput', `with${char}`); - - expect(form.getErrorsMessages()).toContain( - `Remove the character ${char} from the name.` - ); - } catch { - throw Error(`Char "${char}" expected invalid but was allowed`); - } - }; - - await actions.clickSaveForm(); // display form errors - - [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar); - }); - }); - - describe('seeds', () => { - let actions; - let form; - let component; - - beforeEach(async () => { - await act(async () => { - ({ form, actions, component } = setup()); - }); - - component.update(); - - form.setInputValue('remoteClusterFormNameInput', 'remote_cluster_test'); - }); - - test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => { - await actions.clickSaveForm(); // display form errors - - const notInArray = (array) => (value) => array.indexOf(value) < 0; - - const expectInvalidChar = (char) => { - form.setComboBoxValue('remoteClusterFormSeedsInput', `192.16${char}:3000`); - expect(form.getErrorsMessages()).toContain( - `Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.` - ); - }; - - [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] - .filter(notInArray(['-', '_', ':'])) - .forEach(expectInvalidChar); - }); - - test('should require a numeric "port" to be set', async () => { - await actions.clickSaveForm(); - - form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - - form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1:abc'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - }); - }); - - describe('proxy address', () => { - let actions; - let form; - let component; - - beforeEach(async () => { - await act(async () => { - ({ form, actions, component } = setup()); - }); - - component.update(); - - act(() => { - // Enable "proxy" mode - form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); - }); - - component.update(); - }); - - test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => { - await actions.clickSaveForm(); // display form errors - - const notInArray = (array) => (value) => array.indexOf(value) < 0; - - const expectInvalidChar = (char) => { - form.setInputValue('remoteClusterFormProxyAddressInput', `192.16${char}:3000`); - expect(form.getErrorsMessages()).toContain( - 'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.' - ); - }; - - [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] - .filter(notInArray(['-', '_', ':'])) - .forEach(expectInvalidChar); - }); - - test('should require a numeric "port" to be set', async () => { - await actions.clickSaveForm(); - - form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - - form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1:abc'); - expect(form.getErrorsMessages()).toContain('A port is required.'); - }); - }); - }); -}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts new file mode 100644 index 0000000000000..0727bc0c9ba2d --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SinonFakeServer } from 'sinon'; +import { TestBed } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment, RemoteClustersActions } from '../helpers'; +import { setup } from './remote_clusters_add.helpers'; +import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters'; + +const notInArray = (array: string[]) => (value: string) => array.indexOf(value) < 0; + +let component: TestBed['component']; +let actions: RemoteClustersActions; +let server: SinonFakeServer; + +describe('Create Remote cluster', () => { + beforeAll(() => { + ({ server } = setupEnvironment()); + }); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup()); + }); + component.update(); + }); + + describe('on component mount', () => { + test('should have the title of the page set correctly', () => { + expect(actions.pageTitle.exists()).toBe(true); + expect(actions.pageTitle.text()).toEqual('Add remote cluster'); + }); + + test('should have a link to the documentation', () => { + expect(actions.docsButtonExists()).toBe(true); + }); + + test('should have a toggle to Skip unavailable remote cluster', () => { + expect(actions.skipUnavailableSwitch.exists()).toBe(true); + + // By default it should be set to "false" + expect(actions.skipUnavailableSwitch.isChecked()).toBe(false); + + actions.skipUnavailableSwitch.toggle(); + + expect(actions.skipUnavailableSwitch.isChecked()).toBe(true); + }); + + describe('on prem', () => { + test('should have a toggle to enable "proxy" mode for a remote cluster', () => { + expect(actions.connectionModeSwitch.exists()).toBe(true); + + // By default it should be set to "false" + expect(actions.connectionModeSwitch.isChecked()).toBe(false); + + actions.connectionModeSwitch.toggle(); + + expect(actions.connectionModeSwitch.isChecked()).toBe(true); + }); + + test('server name has optional label', () => { + actions.connectionModeSwitch.toggle(); + expect(actions.serverNameInput.getLabel()).toBe('Server name (optional)'); + }); + + test('should display errors and disable the save button when clicking "save" without filling the form', async () => { + expect(actions.globalErrorExists()).toBe(false); + expect(actions.saveButton.isDisabled()).toBe(false); + + await actions.saveButton.click(); + + expect(actions.globalErrorExists()).toBe(true); + expect(actions.getErrorMessages()).toEqual([ + 'Name is required.', + // seeds input is switched on by default on prem and is required + 'At least one seed node is required.', + ]); + expect(actions.saveButton.isDisabled()).toBe(true); + }); + + test('renders no switch for cloud url input and proxy address + server name input modes', () => { + expect(actions.cloudUrlSwitch.exists()).toBe(false); + }); + }); + describe('on cloud', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup(true)); + }); + + component.update(); + }); + + test('renders a switch between cloud url input and proxy address + server name input for proxy connection', () => { + expect(actions.cloudUrlSwitch.exists()).toBe(true); + }); + + test('renders no switch between sniff and proxy modes', () => { + expect(actions.connectionModeSwitch.exists()).toBe(false); + }); + test('defaults to cloud url input for proxy connection', () => { + expect(actions.cloudUrlSwitch.isChecked()).toBe(false); + }); + test('server name has no optional label', () => { + actions.cloudUrlSwitch.toggle(); + expect(actions.serverNameInput.getLabel()).toBe('Server name'); + }); + }); + }); + describe('form validation', () => { + describe('remote cluster name', () => { + test('should not allow spaces', async () => { + actions.nameInput.setValue('with space'); + + await actions.saveButton.click(); + + expect(actions.getErrorMessages()).toContain('Spaces are not allowed in the name.'); + }); + + test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => { + const expectInvalidChar = (char: string) => { + if (char === '-' || char === '_') { + return; + } + + try { + actions.nameInput.setValue(`with${char}`); + + expect(actions.getErrorMessages()).toContain( + `Remove the character ${char} from the name.` + ); + } catch { + throw Error(`Char "${char}" expected invalid but was allowed`); + } + }; + + await actions.saveButton.click(); // display form errors + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar); + }); + }); + + describe('proxy address', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup()); + }); + + component.update(); + + actions.connectionModeSwitch.toggle(); + }); + + test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => { + await actions.saveButton.click(); // display form errors + + const expectInvalidChar = (char: string) => { + actions.proxyAddressInput.setValue(`192.16${char}:3000`); + expect(actions.getErrorMessages()).toContain( + 'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.' + ); + }; + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] + .filter(notInArray(['-', '_', ':'])) + .forEach(expectInvalidChar); + }); + + test('should require a numeric "port" to be set', async () => { + await actions.saveButton.click(); + + actions.proxyAddressInput.setValue('192.168.1.1'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + + actions.proxyAddressInput.setValue('192.168.1.1:abc'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + }); + }); + + describe('on prem', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup()); + }); + + component.update(); + + actions.nameInput.setValue('remote_cluster_test'); + }); + + describe('seeds', () => { + test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => { + await actions.saveButton.click(); // display form errors + + const expectInvalidChar = (char: string) => { + actions.seedsInput.setValue(`192.16${char}:3000`); + expect(actions.getErrorMessages()).toContain( + `Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.` + ); + }; + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] + .filter(notInArray(['-', '_', ':'])) + .forEach(expectInvalidChar); + }); + + test('should require a numeric "port" to be set', async () => { + await actions.saveButton.click(); + + actions.seedsInput.setValue('192.168.1.1'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + + actions.seedsInput.setValue('192.168.1.1:abc'); + expect(actions.getErrorMessages()).toContain('A port is required.'); + }); + }); + + test('server name is optional (proxy connection)', () => { + actions.connectionModeSwitch.toggle(); + actions.saveButton.click(); + expect(actions.getErrorMessages()).toEqual(['A proxy address is required.']); + }); + }); + + describe('on cloud', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup(true)); + }); + + component.update(); + }); + + test('cloud url is required since cloud url input is enabled by default', () => { + actions.saveButton.click(); + expect(actions.getErrorMessages()).toContain('A url is required.'); + }); + + test('proxy address and server name are required when cloud url input is disabled', () => { + actions.cloudUrlSwitch.toggle(); + actions.saveButton.click(); + expect(actions.getErrorMessages()).toEqual([ + 'Name is required.', + 'A proxy address is required.', + 'A server name is required.', + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.ts similarity index 100% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/add/special_characters.ts diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js deleted file mode 100644 index 094fb5056e983..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { registerTestBed } from '@kbn/test/jest'; - -import { RemoteClusterEdit } from '../../../public/application/sections/remote_cluster_edit'; -import { createRemoteClustersStore } from '../../../public/application/store'; -import { registerRouter } from '../../../public/application/services/routing'; - -export const REMOTE_CLUSTER_EDIT_NAME = 'new-york'; - -export const REMOTE_CLUSTER_EDIT = { - name: REMOTE_CLUSTER_EDIT_NAME, - seeds: ['localhost:9400'], - skipUnavailable: true, -}; - -const testBedConfig = { - store: createRemoteClustersStore, - memoryRouter: { - onRouter: (router) => registerRouter(router), - // The remote cluster name to edit is read from the router ":id" param - // so we first set it in our initial entries - initialEntries: [`/${REMOTE_CLUSTER_EDIT_NAME}`], - // and then we declarae the :id param on the component route path - componentRoutePath: '/:name', - }, -}; - -export const setup = registerTestBed(RemoteClusterEdit, testBedConfig); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx new file mode 100644 index 0000000000000..2259396bf33f2 --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; + +import React from 'react'; +import { RemoteClusterEdit } from '../../../public/application/sections'; +import { createRemoteClustersStore } from '../../../public/application/store'; +import { AppRouter, registerRouter } from '../../../public/application/services'; +import { createRemoteClustersActions } from '../helpers'; +import { AppContextProvider } from '../../../public/application/app_context'; + +export const REMOTE_CLUSTER_EDIT_NAME = 'new-york'; + +export const REMOTE_CLUSTER_EDIT = { + name: REMOTE_CLUSTER_EDIT_NAME, + seeds: ['localhost:9400'], + skipUnavailable: true, +}; + +const ComponentWithContext = (props: { isCloudEnabled: boolean }) => { + const { isCloudEnabled, ...rest } = props; + return ( + + + + ); +}; + +const testBedConfig: TestBedConfig = { + store: createRemoteClustersStore, + memoryRouter: { + onRouter: (router: AppRouter) => registerRouter(router), + // The remote cluster name to edit is read from the router ":id" param + // so we first set it in our initial entries + initialEntries: [`/${REMOTE_CLUSTER_EDIT_NAME}`], + // and then we declare the :id param on the component route path + componentRoutePath: '/:name', + }, +}; + +const initTestBed = (isCloudEnabled: boolean) => + registerTestBed(ComponentWithContext, testBedConfig)({ isCloudEnabled }); + +export const setup = async (isCloudEnabled = false) => { + const testBed = await initTestBed(isCloudEnabled); + + return { + ...testBed, + actions: { + ...createRemoteClustersActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js deleted file mode 100644 index 19dd468cb76c5..0000000000000 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js +++ /dev/null @@ -1,80 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { RemoteClusterForm } from '../../../public/application/sections/components/remote_cluster_form'; -import { setupEnvironment } from '../helpers'; -import { setup as setupRemoteClustersAdd } from '../add/remote_clusters_add.helpers'; -import { - setup, - REMOTE_CLUSTER_EDIT, - REMOTE_CLUSTER_EDIT_NAME, -} from './remote_clusters_edit.helpers'; - -describe('Edit Remote cluster', () => { - let component; - let find; - let exists; - - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); - - beforeEach(async () => { - await act(async () => { - ({ component, find, exists } = setup()); - }); - component.update(); - }); - - test('should have the title of the page set correctly', () => { - expect(exists('remoteClusterPageTitle')).toBe(true); - expect(find('remoteClusterPageTitle').text()).toEqual('Edit remote cluster'); - }); - - test('should have a link to the documentation', () => { - expect(exists('remoteClusterDocsButton')).toBe(true); - }); - - /** - * As the "edit" remote cluster component uses the same form underneath that - * the "create" remote cluster, we won't test it again but simply make sure that - * the form component is indeed shared between the 2 app sections. - */ - test('should use the same Form component as the "" component', async () => { - let addRemoteClusterTestBed; - - await act(async () => { - addRemoteClusterTestBed = setupRemoteClustersAdd(); - }); - - addRemoteClusterTestBed.component.update(); - - const formEdit = component.find(RemoteClusterForm); - const formAdd = addRemoteClusterTestBed.component.find(RemoteClusterForm); - - expect(formEdit.length).toBe(1); - expect(formAdd.length).toBe(1); - }); - - test('should populate the form fields with the values from the remote cluster loaded', () => { - expect(find('remoteClusterFormNameInput').props().value).toBe(REMOTE_CLUSTER_EDIT_NAME); - expect(find('remoteClusterFormSeedsInput').text()).toBe(REMOTE_CLUSTER_EDIT.seeds.join('')); - expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe( - REMOTE_CLUSTER_EDIT.skipUnavailable - ); - }); - - test('should disable the form name input', () => { - expect(find('remoteClusterFormNameInput').props().disabled).toBe(true); - }); -}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx new file mode 100644 index 0000000000000..2913de94bc2dd --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +import { RemoteClusterForm } from '../../../public/application/sections/components/remote_cluster_form'; +import { RemoteClustersActions, setupEnvironment } from '../helpers'; +import { setup as setupRemoteClustersAdd } from '../add/remote_clusters_add.helpers'; +import { + setup, + REMOTE_CLUSTER_EDIT, + REMOTE_CLUSTER_EDIT_NAME, +} from './remote_clusters_edit.helpers'; +import { Cluster } from '../../../common/lib'; + +let component: TestBed['component']; +let actions: RemoteClustersActions; +const { server, httpRequestsMockHelpers } = setupEnvironment(); + +describe('Edit Remote cluster', () => { + afterAll(() => { + server.restore(); + }); + + httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); + + beforeEach(async () => { + await act(async () => { + ({ component, actions } = await setup()); + }); + component.update(); + }); + + test('should have the title of the page set correctly', () => { + expect(actions.pageTitle.exists()).toBe(true); + expect(actions.pageTitle.text()).toEqual('Edit remote cluster'); + }); + + test('should have a link to the documentation', () => { + expect(actions.docsButtonExists()).toBe(true); + }); + + /** + * As the "edit" remote cluster component uses the same form underneath that + * the "create" remote cluster, we won't test it again but simply make sure that + * the form component is indeed shared between the 2 app sections. + */ + test('should use the same Form component as the "" component', async () => { + let addRemoteClusterTestBed: TestBed; + + await act(async () => { + addRemoteClusterTestBed = await setupRemoteClustersAdd(); + }); + + addRemoteClusterTestBed!.component.update(); + + const formEdit = component.find(RemoteClusterForm); + const formAdd = addRemoteClusterTestBed!.component.find(RemoteClusterForm); + + expect(formEdit.length).toBe(1); + expect(formAdd.length).toBe(1); + }); + + test('should populate the form fields with the values from the remote cluster loaded', () => { + expect(actions.nameInput.getValue()).toBe(REMOTE_CLUSTER_EDIT_NAME); + // seeds input for sniff connection is not shown on Cloud + expect(actions.seedsInput.getValue()).toBe(REMOTE_CLUSTER_EDIT.seeds.join('')); + expect(actions.skipUnavailableSwitch.isChecked()).toBe(REMOTE_CLUSTER_EDIT.skipUnavailable); + }); + + test('should disable the form name input', () => { + expect(actions.nameInput.isDisabled()).toBe(true); + }); + + describe('on cloud', () => { + const cloudUrl = 'cloud-url'; + const defaultCloudPort = '9400'; + test('existing cluster that defaults to cloud url (default port)', async () => { + const cluster: Cluster = { + name: REMOTE_CLUSTER_EDIT_NAME, + mode: 'proxy', + proxyAddress: `${cloudUrl}:${defaultCloudPort}`, + serverName: cloudUrl, + }; + httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); + + await act(async () => { + ({ component, actions } = await setup(true)); + }); + component.update(); + + expect(actions.cloudUrlInput.exists()).toBe(true); + expect(actions.cloudUrlInput.getValue()).toBe(cloudUrl); + }); + + test('existing cluster that defaults to manual input (non-default port)', async () => { + const cluster: Cluster = { + name: REMOTE_CLUSTER_EDIT_NAME, + mode: 'proxy', + proxyAddress: `${cloudUrl}:9500`, + serverName: cloudUrl, + }; + httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); + + await act(async () => { + ({ component, actions } = await setup(true)); + }); + component.update(); + + expect(actions.cloudUrlInput.exists()).toBe(false); + + expect(actions.proxyAddressInput.exists()).toBe(true); + expect(actions.serverNameInput.exists()).toBe(true); + }); + + test('existing cluster that defaults to manual input (proxy address is different from server name)', async () => { + const cluster: Cluster = { + name: REMOTE_CLUSTER_EDIT_NAME, + mode: 'proxy', + proxyAddress: `${cloudUrl}:${defaultCloudPort}`, + serverName: 'another-value', + }; + httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); + + await act(async () => { + ({ component, actions } = await setup(true)); + }); + component.update(); + + expect(actions.cloudUrlInput.exists()).toBe(false); + + expect(actions.proxyAddressInput.exists()).toBe(true); + expect(actions.serverNameInput.exists()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts similarity index 63% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts index 304ec51986aba..3ebe3ab5738d6 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts @@ -5,25 +5,24 @@ * 2.0. */ -import sinon from 'sinon'; +import sinon, { SinonFakeServer } from 'sinon'; +import { Cluster } from '../../../common/lib'; // Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server) => { - const mockResponse = (response) => [ +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const mockResponse = (response: Cluster[] | { itemsDeleted: string[]; errors: string[] }) => [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), ]; - const setLoadRemoteClustersResponse = (response) => { - server.respondWith('GET', '/api/remote_clusters', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); + const setLoadRemoteClustersResponse = (response: Cluster[] = []) => { + server.respondWith('GET', '/api/remote_clusters', mockResponse(response)); }; - const setDeleteRemoteClusterResponse = (response) => { + const setDeleteRemoteClusterResponse = ( + response: { itemsDeleted: string[]; errors: string[] } = { itemsDeleted: [], errors: [] } + ) => { server.respondWith('DELETE', /api\/remote_clusters/, mockResponse(response)); }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts similarity index 80% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts index 63084b21e3902..cf859ff6913f5 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts @@ -7,3 +7,4 @@ export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest'; export { setupEnvironment } from './setup_environment'; +export { createRemoteClustersActions, RemoteClustersActions } from './remote_clusters_actions'; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts new file mode 100644 index 0000000000000..ba0c424793838 --- /dev/null +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TestBed } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +export interface RemoteClustersActions { + docsButtonExists: () => boolean; + pageTitle: { + exists: () => boolean; + text: () => string; + }; + nameInput: { + setValue: (name: string) => void; + getValue: () => string; + isDisabled: () => boolean; + }; + skipUnavailableSwitch: { + exists: () => boolean; + toggle: () => void; + isChecked: () => boolean; + }; + connectionModeSwitch: { + exists: () => boolean; + toggle: () => void; + isChecked: () => boolean; + }; + cloudUrlSwitch: { + toggle: () => void; + exists: () => boolean; + isChecked: () => boolean; + }; + cloudUrlInput: { + exists: () => boolean; + getValue: () => string; + }; + seedsInput: { + setValue: (seed: string) => void; + getValue: () => string; + }; + proxyAddressInput: { + setValue: (proxyAddress: string) => void; + exists: () => boolean; + }; + serverNameInput: { + getLabel: () => string; + exists: () => boolean; + }; + saveButton: { + click: () => void; + isDisabled: () => boolean; + }; + getErrorMessages: () => string[]; + globalErrorExists: () => boolean; +} +export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersActions => { + const { form, exists, find, component } = testBed; + + const docsButtonExists = () => exists('remoteClusterDocsButton'); + const createPageTitleActions = () => { + const pageTitleSelector = 'remoteClusterPageTitle'; + return { + pageTitle: { + exists: () => exists(pageTitleSelector), + text: () => find(pageTitleSelector).text(), + }, + }; + }; + const createNameInputActions = () => { + const nameInputSelector = 'remoteClusterFormNameInput'; + return { + nameInput: { + setValue: (name: string) => form.setInputValue(nameInputSelector, name), + getValue: () => find(nameInputSelector).props().value, + isDisabled: () => find(nameInputSelector).props().disabled, + }, + }; + }; + + const createSkipUnavailableActions = () => { + const skipUnavailableToggleSelector = 'remoteClusterFormSkipUnavailableFormToggle'; + return { + skipUnavailableSwitch: { + exists: () => exists(skipUnavailableToggleSelector), + toggle: () => { + act(() => { + form.toggleEuiSwitch(skipUnavailableToggleSelector); + }); + component.update(); + }, + isChecked: () => find(skipUnavailableToggleSelector).props()['aria-checked'], + }, + }; + }; + + const createConnectionModeActions = () => { + const connectionModeToggleSelector = 'remoteClusterFormConnectionModeToggle'; + return { + connectionModeSwitch: { + exists: () => exists(connectionModeToggleSelector), + toggle: () => { + act(() => { + form.toggleEuiSwitch(connectionModeToggleSelector); + }); + component.update(); + }, + isChecked: () => find(connectionModeToggleSelector).props()['aria-checked'], + }, + }; + }; + + const createCloudUrlSwitchActions = () => { + const cloudUrlSelector = 'remoteClusterFormCloudUrlToggle'; + return { + cloudUrlSwitch: { + exists: () => exists(cloudUrlSelector), + toggle: () => { + act(() => { + form.toggleEuiSwitch(cloudUrlSelector); + }); + component.update(); + }, + isChecked: () => find(cloudUrlSelector).props()['aria-checked'], + }, + }; + }; + + const createSeedsInputActions = () => { + const seedsInputSelector = 'remoteClusterFormSeedsInput'; + return { + seedsInput: { + setValue: (seed: string) => form.setComboBoxValue(seedsInputSelector, seed), + getValue: () => find(seedsInputSelector).text(), + }, + }; + }; + + const createProxyAddressActions = () => { + const proxyAddressSelector = 'remoteClusterFormProxyAddressInput'; + return { + proxyAddressInput: { + setValue: (proxyAddress: string) => form.setInputValue(proxyAddressSelector, proxyAddress), + exists: () => exists(proxyAddressSelector), + }, + }; + }; + + const createSaveButtonActions = () => { + const click = () => { + act(() => { + find('remoteClusterFormSaveButton').simulate('click'); + }); + + component.update(); + }; + const isDisabled = () => find('remoteClusterFormSaveButton').props().disabled; + return { saveButton: { click, isDisabled } }; + }; + + const createServerNameActions = () => { + const serverNameSelector = 'remoteClusterFormServerNameFormRow'; + return { + serverNameInput: { + getLabel: () => find('remoteClusterFormServerNameFormRow').find('label').text(), + exists: () => exists(serverNameSelector), + }, + }; + }; + + const globalErrorExists = () => exists('remoteClusterFormGlobalError'); + + const createCloudUrlInputActions = () => { + const cloudUrlInputSelector = 'remoteClusterFormCloudUrlInput'; + return { + cloudUrlInput: { + exists: () => exists(cloudUrlInputSelector), + getValue: () => find(cloudUrlInputSelector).props().value, + }, + }; + }; + return { + docsButtonExists, + ...createPageTitleActions(), + ...createNameInputActions(), + ...createSkipUnavailableActions(), + ...createConnectionModeActions(), + ...createCloudUrlSwitchActions(), + ...createSeedsInputActions(), + ...createCloudUrlInputActions(), + ...createProxyAddressActions(), + ...createServerNameActions(), + ...createSaveButtonActions(), + getErrorMessages: form.getErrorsMessages, + globalErrorExists, + }; +}; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts similarity index 95% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts index 97ad344a63cc4..084552c5e6abe 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts @@ -36,6 +36,8 @@ export const setupEnvironment = () => { notificationServiceMock.createSetupContract().toasts, fatalErrorsServiceMock.createSetupContract() ); + // This expects HttpSetup but we're giving it AxiosInstance. + // @ts-ignore initHttp(mockHttpClient); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/remote_clusters/public/application/app_context.tsx b/x-pack/plugins/remote_clusters/public/application/app_context.tsx index 7931001c6faee..528ec322f49e1 100644 --- a/x-pack/plugins/remote_clusters/public/application/app_context.tsx +++ b/x-pack/plugins/remote_clusters/public/application/app_context.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { createContext } from 'react'; +import React, { createContext, useContext } from 'react'; export interface Context { isCloudEnabled: boolean; + cloudBaseUrl: string; } export const AppContext = createContext({} as any); @@ -22,3 +23,10 @@ export const AppContextProvider = ({ }) => { return {children}; }; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) throw new Error('Cannot use outside of app context'); + + return ctx; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/index.d.ts b/x-pack/plugins/remote_clusters/public/application/index.d.ts index 167297cedf556..45f981b5f2bc5 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.d.ts +++ b/x-pack/plugins/remote_clusters/public/application/index.d.ts @@ -12,7 +12,8 @@ export declare const renderApp: ( elem: HTMLElement | null, I18nContext: I18nStart['Context'], appDependencies: { - isCloudEnabled?: boolean; + isCloudEnabled: boolean; + cloudBaseUrl: string; }, history: ScopedHistory ) => ReturnType; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap deleted file mode 100644 index 5f09193be90c2..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ /dev/null @@ -1,2017 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RemoteClusterForm proxy mode renders correct connection settings when user enables proxy mode 1`] = ` - - -
    - - } - fullWidth={true} - title={ - -

    - -

    -
    - } - > -
    - -
    - -
    - - -

    - - Name - -

    -
    -
    - -
    - -
    - - A unique name for the cluster. - -
    -
    -
    -
    -
    -
    - -
    - - } - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - helpText={ - - } - isInvalid={false} - label={ - - } - labelType="label" - > -
    -
    - - - -
    -
    - - -
    -
    - - - - -
    -
    -
    -
    - -
    - - Name can only contain letters, numbers, underscores, and dashes. - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - } - onChange={[Function]} - /> - - - } - fullWidth={true} - title={ - -

    - -

    -
    - } - > -
    - -
    - -
    - - -

    - - Connection mode - -

    -
    -
    - -
    - -
    - - Use seed nodes by default, or switch to proxy mode. - - -
    -
    - - } - onBlur={[Function]} - onChange={[Function]} - onFocus={[Function]} - > -
    - - - - Use proxy mode - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - } - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - helpText={ - - } - isInvalid={false} - label={ - - } - labelType="label" - > -
    -
    - - - -
    -
    - - -
    -
    - - - - -
    -
    -
    -
    - -
    - - The address to use for remote connections. - -
    -
    -
    -
    -
    - - - , - } - } - /> - } - isInvalid={false} - label={ - - } - labelType="label" - > -
    -
    - - - -
    -
    - - -
    -
    - - - - -
    -
    -
    -
    - -
    - - - , - } - } - > - A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. - - - - -
    -
    -
    -
    -
    - - } - label={ - - } - labelType="label" - > -
    -
    - - - -
    -
    - - -
    -
    - - - - -
    -
    -
    -
    - -
    - - The number of socket connections to open per remote cluster. - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -

    - - - , - "optionName": - - , - } - } - /> -

    - - } - fullWidth={true} - title={ - -

    - -

    -
    - } - > -
    - -
    - -
    - - -

    - - Make remote cluster optional - -

    -
    -
    - -
    - -
    -

    - - - , - "optionName": - - , - } - } - > - A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable - - - Skip if unavailable - - - . - - - - -

    -
    -
    -
    -
    -
    -
    - -
    - -
    -
    - -
    - - - Skip if unavailable - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - -
    - -
    - -
    - -
    - - - - - -
    -
    -
    -
    -
    -
    - -
    - - - -
    -
    -
    -
    - -`; - -exports[`RemoteClusterForm renders untouched state 1`] = ` -Array [ -
    -
    -
    -
    -

    - Name -

    -
    -
    - A unique name for the cluster. -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - Name can only contain letters, numbers, underscores, and dashes. -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Connection mode -

    -
    -
    - Use seed nodes by default, or switch to proxy mode. -
    -
    -
    - - - Use proxy mode - -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - The number of gateway nodes to connect to for this cluster. -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Make remote cluster optional -

    -
    -
    -

    - A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable - - Skip if unavailable - - . - -

    -
    -
    -
    -
    -
    -
    -
    - - - Skip if unavailable - -
    -
    -
    -
    -
    -
    -
    , -
    , -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    , -] -`; - -exports[`RemoteClusterForm validation renders invalid state and a global form error when the user tries to submit an invalid form 1`] = ` -Array [ -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - Name is required. -
    -
    - Name can only contain letters, numbers, underscores, and dashes. -
    -
    -
    , -
    -
    - -
    -
    - -
    , -
    -
    -
    - - - Skip if unavailable - -
    -
    -
    , -
    , -] -`; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx new file mode 100644 index 0000000000000..1d4862ff094ce --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/cloud_url_help.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink, EuiPopover, EuiPopoverTitle, EuiText } from '@elastic/eui'; +import { useAppContext } from '../../../../app_context'; + +export const CloudUrlHelp: FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + const { cloudBaseUrl } = useAppContext(); + return ( + + { + setIsOpen(!isOpen); + }} + > + + + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="upCenter" + > + + + + + + + + ), + elasticsearch: Elasticsearch, + }} + /> + + + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.tsx new file mode 100644 index 0000000000000..d06b4f111ec92 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/connection_mode.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiDescribedFormGroup, EuiTitle, EuiFormRow, EuiSwitch, EuiSpacer } from '@elastic/eui'; + +import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants'; +import { useAppContext } from '../../../../app_context'; + +import { ClusterErrors } from '../validators'; +import { SniffConnection } from './sniff_connection'; +import { ProxyConnection } from './proxy_connection'; +import { FormFields } from '../remote_cluster_form'; + +export interface Props { + fields: FormFields; + onFieldsChange: (fields: Partial) => void; + fieldsErrors: ClusterErrors; + areErrorsVisible: boolean; +} + +export const ConnectionMode: FunctionComponent = (props) => { + const { fields, onFieldsChange } = props; + const { mode, cloudUrlEnabled } = fields; + const { isCloudEnabled } = useAppContext(); + + return ( + +

    + +

    + + } + description={ + <> + {isCloudEnabled ? ( + <> + + + + } + checked={!cloudUrlEnabled} + data-test-subj="remoteClusterFormCloudUrlToggle" + onChange={(e) => onFieldsChange({ cloudUrlEnabled: !e.target.checked })} + /> + + + + ) : ( + <> + + + + } + checked={mode === PROXY_MODE} + data-test-subj="remoteClusterFormConnectionModeToggle" + onChange={(e) => + onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE }) + } + /> + + + )} + + } + fullWidth + > + {mode === SNIFF_MODE ? : } +
    + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts new file mode 100644 index 0000000000000..864385ad0b1a3 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ConnectionMode } from './connection_mode'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx new file mode 100644 index 0000000000000..04e8533a0d2af --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/proxy_connection.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldNumber, EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { useAppContext } from '../../../../app_context'; +import { proxySettingsUrl } from '../../../../services/documentation'; +import { Props } from './connection_mode'; +import { CloudUrlHelp } from './cloud_url_help'; + +export const ProxyConnection: FunctionComponent = (props) => { + const { fields, fieldsErrors, areErrorsVisible, onFieldsChange } = props; + const { isCloudEnabled } = useAppContext(); + const { proxyAddress, serverName, proxySocketConnections, cloudUrl, cloudUrlEnabled } = fields; + const { + proxyAddress: proxyAddressError, + serverName: serverNameError, + cloudUrl: cloudUrlError, + } = fieldsErrors; + + return ( + <> + {cloudUrlEnabled ? ( + <> + + } + labelAppend={} + isInvalid={Boolean(areErrorsVisible && cloudUrlError)} + error={cloudUrlError} + fullWidth + helpText={ + + } + > + onFieldsChange({ cloudUrl: e.target.value })} + isInvalid={Boolean(areErrorsVisible && cloudUrlError)} + data-test-subj="remoteClusterFormCloudUrlInput" + fullWidth + /> + + + ) : ( + <> + + } + helpText={ + + } + isInvalid={Boolean(areErrorsVisible && proxyAddressError)} + error={proxyAddressError} + fullWidth + > + onFieldsChange({ proxyAddress: e.target.value })} + isInvalid={Boolean(areErrorsVisible && proxyAddressError)} + data-test-subj="remoteClusterFormProxyAddressInput" + fullWidth + /> + + + + ) : ( + + ) + } + helpText={ + + + + ), + }} + /> + } + fullWidth + > + onFieldsChange({ serverName: e.target.value })} + isInvalid={Boolean(areErrorsVisible && serverNameError)} + fullWidth + /> + + + )} + + } + helpText={ + + } + fullWidth + > + onFieldsChange({ proxySocketConnections: Number(e.target.value) })} + fullWidth + /> + + + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.tsx new file mode 100644 index 0000000000000..063aeb3490aef --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/components/sniff_connection.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFieldNumber, + EuiFormRow, + EuiLink, +} from '@elastic/eui'; + +import { transportPortUrl } from '../../../../services/documentation'; +import { validateSeed } from '../validators'; +import { Props } from './connection_mode'; + +export const SniffConnection: FunctionComponent = ({ + fields, + fieldsErrors, + areErrorsVisible, + onFieldsChange, +}) => { + const [localSeedErrors, setLocalSeedErrors] = useState([]); + const { seeds = [], nodeConnections } = fields; + const { seeds: seedsError } = fieldsErrors; + // Show errors if there is a general form error or local errors. + const areFormErrorsVisible = Boolean(areErrorsVisible && seedsError); + const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0; + const errors = + areFormErrorsVisible && seedsError ? localSeedErrors.concat(seedsError) : localSeedErrors; + const formattedSeeds: EuiComboBoxOptionOption[] = seeds.map((seed: string) => ({ label: seed })); + + const onCreateSeed = (newSeed?: string) => { + // If the user just hit enter without typing anything, treat it as a no-op. + if (!newSeed) { + return; + } + + const validationErrors = validateSeed(newSeed); + + if (validationErrors.length !== 0) { + setLocalSeedErrors(validationErrors); + // Return false to explicitly reject the user's input. + return false; + } + + const newSeeds = seeds.slice(0); + newSeeds.push(newSeed.toLowerCase()); + onFieldsChange({ seeds: newSeeds }); + }; + + const onSeedsInputChange = (seedInput?: string) => { + if (!seedInput) { + // If empty seedInput ("") don't do anything. This happens + // right after a seed is created. + return; + } + + // Allow typing to clear the errors, but not to add new ones. + const validationErrors = + !seedInput || validateSeed(seedInput).length === 0 ? [] : localSeedErrors; + + // EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the + // input is a duplicate. So we need to surface this error here instead. + const isDuplicate = seeds.includes(seedInput); + + if (isDuplicate) { + validationErrors.push( + + ); + } + + setLocalSeedErrors(validationErrors); + }; + return ( + <> + + } + helpText={ + + + + ), + }} + /> + } + isInvalid={showErrors} + error={errors} + fullWidth + > + + onFieldsChange({ seeds: options.map(({ label }) => label) }) + } + onSearchChange={onSeedsInputChange} + isInvalid={showErrors} + fullWidth + data-test-subj="remoteClusterFormSeedsInput" + /> + + + + } + helpText={ + + } + fullWidth + > + onFieldsChange({ nodeConnections: Number(e.target.value) })} + fullWidth + /> + + + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.ts similarity index 100% rename from x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.js rename to x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/index.ts diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js deleted file mode 100644 index 325215d08af5f..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js +++ /dev/null @@ -1,962 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { merge } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiComboBox, - EuiDescribedFormGroup, - EuiFieldNumber, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiLink, - EuiLoadingKibana, - EuiLoadingSpinner, - EuiOverlayMask, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTitle, - EuiDelayRender, - EuiScreenReaderOnly, - htmlIdGenerator, -} from '@elastic/eui'; - -import { - skippingDisconnectedClustersUrl, - transportPortUrl, - proxySettingsUrl, -} from '../../../services/documentation'; - -import { RequestFlyout } from './request_flyout'; - -import { - validateName, - validateSeeds, - validateProxy, - validateSeed, - validateServerName, -} from './validators'; - -import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants'; - -import { AppContext } from '../../../app_context'; - -const defaultFields = { - name: '', - seeds: [], - skipUnavailable: false, - nodeConnections: 3, - proxyAddress: '', - proxySocketConnections: 18, - serverName: '', -}; - -const ERROR_TITLE_ID = 'removeClustersErrorTitle'; -const ERROR_LIST_ID = 'removeClustersErrorList'; - -export class RemoteClusterForm extends Component { - static propTypes = { - save: PropTypes.func.isRequired, - cancel: PropTypes.func, - isSaving: PropTypes.bool, - saveError: PropTypes.object, - fields: PropTypes.object, - disabledFields: PropTypes.object, - }; - - static defaultProps = { - fields: merge({}, defaultFields), - disabledFields: {}, - }; - - static contextType = AppContext; - - constructor(props, context) { - super(props, context); - - const { fields, disabledFields } = props; - const { isCloudEnabled } = context; - - // Connection mode should default to "proxy" in cloud - const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE; - const fieldsState = merge({}, { ...defaultFields, mode: defaultMode }, fields); - - this.generateId = htmlIdGenerator(); - this.state = { - localSeedErrors: [], - seedInput: '', - fields: fieldsState, - disabledFields, - fieldsErrors: this.getFieldsErrors(fieldsState), - areErrorsVisible: false, - isRequestVisible: false, - }; - } - - toggleRequest = () => { - this.setState(({ isRequestVisible }) => ({ - isRequestVisible: !isRequestVisible, - })); - }; - - getFieldsErrors(fields, seedInput = '') { - const { name, seeds, mode, proxyAddress, serverName } = fields; - const { isCloudEnabled } = this.context; - - return { - name: validateName(name), - seeds: mode === SNIFF_MODE ? validateSeeds(seeds, seedInput) : null, - proxyAddress: mode === PROXY_MODE ? validateProxy(proxyAddress) : null, - // server name is only required in cloud when proxy mode is enabled - serverName: isCloudEnabled && mode === PROXY_MODE ? validateServerName(serverName) : null, - }; - } - - onFieldsChange = (changedFields) => { - this.setState(({ fields: prevFields, seedInput }) => { - const newFields = { - ...prevFields, - ...changedFields, - }; - return { - fields: newFields, - fieldsErrors: this.getFieldsErrors(newFields, seedInput), - }; - }); - }; - - getAllFields() { - const { - fields: { - name, - mode, - seeds, - nodeConnections, - proxyAddress, - proxySocketConnections, - serverName, - skipUnavailable, - }, - } = this.state; - const { fields } = this.props; - - let modeSettings; - - if (mode === PROXY_MODE) { - modeSettings = { - proxyAddress, - proxySocketConnections, - serverName, - }; - } else { - modeSettings = { - seeds, - nodeConnections, - }; - } - - return { - name, - skipUnavailable, - mode, - hasDeprecatedProxySetting: fields.hasDeprecatedProxySetting, - ...modeSettings, - }; - } - - save = () => { - const { save } = this.props; - - if (this.hasErrors()) { - this.setState({ - areErrorsVisible: true, - }); - return; - } - - const cluster = this.getAllFields(); - save(cluster); - }; - - onCreateSeed = (newSeed) => { - // If the user just hit enter without typing anything, treat it as a no-op. - if (!newSeed) { - return; - } - - const localSeedErrors = validateSeed(newSeed); - - if (localSeedErrors.length !== 0) { - this.setState({ - localSeedErrors, - }); - - // Return false to explicitly reject the user's input. - return false; - } - - const { - fields: { seeds }, - } = this.state; - - const newSeeds = seeds.slice(0); - newSeeds.push(newSeed.toLowerCase()); - this.onFieldsChange({ seeds: newSeeds }); - }; - - onSeedsInputChange = (seedInput) => { - if (!seedInput) { - // If empty seedInput ("") don't do anything. This happens - // right after a seed is created. - return; - } - - this.setState(({ fields, localSeedErrors }) => { - const { seeds } = fields; - - // Allow typing to clear the errors, but not to add new ones. - const errors = !seedInput || validateSeed(seedInput).length === 0 ? [] : localSeedErrors; - - // EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the - // input is a duplicate. So we need to surface this error here instead. - const isDuplicate = seeds.includes(seedInput); - - if (isDuplicate) { - errors.push( - i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage', { - defaultMessage: `Duplicate seed nodes aren't allowed.`, - }) - ); - } - - return { - localSeedErrors: errors, - fieldsErrors: this.getFieldsErrors(fields, seedInput), - seedInput, - }; - }); - }; - - onSeedsChange = (seeds) => { - this.onFieldsChange({ seeds: seeds.map(({ label }) => label) }); - }; - - onSkipUnavailableChange = (e) => { - const skipUnavailable = e.target.checked; - this.onFieldsChange({ skipUnavailable }); - }; - - resetToDefault = (fieldName) => { - this.onFieldsChange({ - [fieldName]: defaultFields[fieldName], - }); - }; - - hasErrors = () => { - const { fieldsErrors, localSeedErrors } = this.state; - const errorValues = Object.values(fieldsErrors); - const hasErrors = errorValues.some((error) => error != null) || localSeedErrors.length; - return hasErrors; - }; - - renderSniffModeSettings() { - const { - areErrorsVisible, - fields: { seeds, nodeConnections }, - fieldsErrors: { seeds: errorsSeeds }, - localSeedErrors, - } = this.state; - - // Show errors if there is a general form error or local errors. - const areFormErrorsVisible = Boolean(areErrorsVisible && errorsSeeds); - const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0; - const errors = areFormErrorsVisible ? localSeedErrors.concat(errorsSeeds) : localSeedErrors; - - const formattedSeeds = seeds.map((seed) => ({ label: seed })); - - return ( - <> - - } - helpText={ - - - - ), - }} - /> - } - isInvalid={showErrors} - error={errors} - fullWidth - > - - - - - } - helpText={ - - } - fullWidth - > - - this.onFieldsChange({ nodeConnections: Number(e.target.value) || null }) - } - fullWidth - /> - - - ); - } - - renderProxyModeSettings() { - const { - areErrorsVisible, - fields: { proxyAddress, proxySocketConnections, serverName }, - fieldsErrors: { proxyAddress: errorProxyAddress, serverName: errorServerName }, - } = this.state; - - const { isCloudEnabled } = this.context; - - return ( - <> - - } - helpText={ - - } - isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} - error={errorProxyAddress} - fullWidth - > - this.onFieldsChange({ proxyAddress: e.target.value })} - isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} - data-test-subj="remoteClusterFormProxyAddressInput" - fullWidth - /> - - - - ) : ( - - ) - } - helpText={ - - - - ), - }} - /> - } - fullWidth - > - this.onFieldsChange({ serverName: e.target.value })} - isInvalid={Boolean(areErrorsVisible && errorServerName)} - fullWidth - /> - - - - } - helpText={ - - } - fullWidth - > - - this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null }) - } - fullWidth - /> - - - ); - } - - renderMode() { - const { - fields: { mode }, - } = this.state; - - const { isCloudEnabled } = this.context; - - return ( - -

    - -

    - - } - description={ - <> - - - - } - checked={mode === PROXY_MODE} - data-test-subj="remoteClusterFormConnectionModeToggle" - onChange={(e) => - this.onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE }) - } - /> - - {isCloudEnabled && mode === PROXY_MODE ? ( - <> - - - } - > - - - - ), - searchString: ( - - - - ), - }} - /> - - - ) : null} - - } - fullWidth - > - {mode === PROXY_MODE ? this.renderProxyModeSettings() : this.renderSniffModeSettings()} -
    - ); - } - - renderSkipUnavailable() { - const { - fields: { skipUnavailable }, - } = this.state; - - return ( - -

    - -

    - - } - description={ - -

    - - - - ), - learnMoreLink: ( - - - - ), - }} - /> -

    -
    - } - fullWidth - > - { - this.resetToDefault('skipUnavailable'); - }} - > - - - ) : null - } - > - - -
    - ); - } - - renderActions() { - const { isSaving, cancel } = this.props; - const { areErrorsVisible, isRequestVisible } = this.state; - - if (isSaving) { - return ( - - - - - - - - - - - - ); - } - - let cancelButton; - - if (cancel) { - cancelButton = ( - - - - - - ); - } - - const isSaveDisabled = areErrorsVisible && this.hasErrors(); - - return ( - - - - - - - - - - {cancelButton} - - - - - - {isRequestVisible ? ( - - ) : ( - - )} - - - - ); - } - - renderSavingFeedback() { - if (this.props.isSaving) { - return ( - - - - ); - } - - return null; - } - - renderSaveErrorFeedback() { - const { saveError } = this.props; - - if (saveError) { - const { message, cause } = saveError; - - let errorBody; - - if (cause && Array.isArray(cause)) { - if (cause.length === 1) { - errorBody =

    {cause[0]}

    ; - } else { - errorBody = ( -
      - {cause.map((causeValue) => ( -
    • {causeValue}
    • - ))} -
    - ); - } - } - - return ( - - - {errorBody} - - - - - ); - } - - return null; - } - - renderErrors = () => { - const { - areErrorsVisible, - fieldsErrors: { name: errorClusterName, seeds: errorsSeeds, proxyAddress: errorProxyAddress }, - localSeedErrors, - } = this.state; - - const hasErrors = this.hasErrors(); - - if (!areErrorsVisible || !hasErrors) { - return null; - } - - const errorExplanations = []; - - if (errorClusterName) { - errorExplanations.push({ - key: 'nameExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', { - defaultMessage: 'The "Name" field is invalid.', - }), - error: errorClusterName, - }); - } - - if (errorsSeeds) { - errorExplanations.push({ - key: 'seedsExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', { - defaultMessage: 'The "Seed nodes" field is invalid.', - }), - error: errorsSeeds, - }); - } - - if (localSeedErrors && localSeedErrors.length) { - errorExplanations.push({ - key: 'localSeedExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage', { - defaultMessage: 'The "Seed nodes" field is invalid.', - }), - error: localSeedErrors.join(' '), - }); - } - - if (errorProxyAddress) { - errorExplanations.push({ - key: 'seedsExplanation', - field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', { - defaultMessage: 'The "Proxy address" field is invalid.', - }), - error: errorProxyAddress, - }); - } - - const messagesToBeRendered = errorExplanations.length && ( - -
    - {errorExplanations.map(({ key, field, error }) => ( -
    -
    {field}
    -
    {error}
    -
    - ))} -
    -
    - ); - - return ( - - - - - - } - color="danger" - iconType="cross" - /> - {messagesToBeRendered} - - ); - }; - - render() { - const { - disabledFields: { name: disabledName }, - } = this.props; - - const { - isRequestVisible, - areErrorsVisible, - fields: { name }, - fieldsErrors: { name: errorClusterName }, - } = this.state; - - return ( - - {this.renderSaveErrorFeedback()} - - - -

    - -

    - - } - description={ - - } - fullWidth - > - - } - helpText={ - - } - error={errorClusterName} - isInvalid={Boolean(areErrorsVisible && errorClusterName)} - fullWidth - > - this.onFieldsChange({ name: e.target.value })} - fullWidth - disabled={disabledName} - data-test-subj="remoteClusterFormNameInput" - /> - -
    - - {this.renderMode()} - - {this.renderSkipUnavailable()} -
    - - {this.renderErrors()} - - - - {this.renderActions()} - - {this.renderSavingFeedback()} - - {isRequestVisible ? ( - this.setState({ isRequestVisible: false })} - /> - ) : null} -
    - ); - } -} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js deleted file mode 100644 index 2ae16b8ca7cbf..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mountWithIntl, renderWithIntl } from '@kbn/test/jest'; -import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; -import { RemoteClusterForm } from './remote_cluster_form'; - -// Make sure we have deterministic aria IDs. -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: (prefix = 'staticGenerator') => (suffix = 'staticId') => `${prefix}_${suffix}`, -})); - -describe('RemoteClusterForm', () => { - test(`renders untouched state`, () => { - const component = renderWithIntl( {}} />); - expect(component).toMatchSnapshot(); - }); - - describe('proxy mode', () => { - test('renders correct connection settings when user enables proxy mode', () => { - const component = mountWithIntl( {}} />); - - findTestSubject(component, 'remoteClusterFormConnectionModeToggle').simulate('click'); - - expect(component).toMatchSnapshot(); - }); - }); - - describe('validation', () => { - test('renders invalid state and a global form error when the user tries to submit an invalid form', () => { - const component = mountWithIntl( {}} />); - - findTestSubject(component, 'remoteClusterFormSaveButton').simulate('click'); - - const fieldsSnapshot = [ - 'remoteClusterFormNameFormRow', - 'remoteClusterFormSeedNodesFormRow', - 'remoteClusterFormSkipUnavailableFormRow', - 'remoteClusterFormGlobalError', - ].map((testSubject) => { - const mountedField = findTestSubject(component, testSubject); - return takeMountedSnapshot(mountedField); - }); - - expect(fieldsSnapshot).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx new file mode 100644 index 0000000000000..9f6eee757c755 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx @@ -0,0 +1,629 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component, Fragment } from 'react'; +import { merge } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiDescribedFormGroup, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiLoadingKibana, + EuiLoadingSpinner, + EuiOverlayMask, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, + EuiDelayRender, + EuiScreenReaderOnly, + htmlIdGenerator, + EuiSwitchEvent, +} from '@elastic/eui'; + +import { Cluster } from '../../../../../common/lib'; +import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants'; + +import { AppContext, Context } from '../../../app_context'; + +import { skippingDisconnectedClustersUrl } from '../../../services/documentation'; + +import { RequestFlyout } from './request_flyout'; +import { ConnectionMode } from './components'; +import { + ClusterErrors, + convertCloudUrlToProxyConnection, + convertProxyConnectionToCloudUrl, + validateCluster, +} from './validators'; +import { isCloudUrlEnabled } from './validators/validate_cloud_url'; + +const defaultClusterValues: Cluster = { + name: '', + seeds: [], + skipUnavailable: false, + nodeConnections: 3, + proxyAddress: '', + proxySocketConnections: 18, + serverName: '', +}; + +const ERROR_TITLE_ID = 'removeClustersErrorTitle'; +const ERROR_LIST_ID = 'removeClustersErrorList'; + +interface Props { + save: (cluster: Cluster) => void; + cancel?: () => void; + isSaving?: boolean; + saveError?: any; + cluster?: Cluster; +} + +export type FormFields = Cluster & { cloudUrl: string; cloudUrlEnabled: boolean }; + +interface State { + fields: FormFields; + fieldsErrors: ClusterErrors; + areErrorsVisible: boolean; + isRequestVisible: boolean; +} + +export class RemoteClusterForm extends Component { + static defaultProps = { + fields: merge({}, defaultClusterValues), + }; + + static contextType = AppContext; + private readonly generateId: (idSuffix?: string) => string; + + constructor(props: Props, context: Context) { + super(props, context); + + const { cluster } = props; + const { isCloudEnabled } = context; + + // Connection mode should default to "proxy" in cloud + const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE; + const fieldsState: FormFields = merge( + {}, + { + ...defaultClusterValues, + mode: defaultMode, + cloudUrl: convertProxyConnectionToCloudUrl(cluster), + cloudUrlEnabled: isCloudEnabled && isCloudUrlEnabled(cluster), + }, + cluster + ); + + this.generateId = htmlIdGenerator(); + this.state = { + fields: fieldsState, + fieldsErrors: validateCluster(fieldsState, isCloudEnabled), + areErrorsVisible: false, + isRequestVisible: false, + }; + } + + toggleRequest = () => { + this.setState(({ isRequestVisible }) => ({ + isRequestVisible: !isRequestVisible, + })); + }; + + onFieldsChange = (changedFields: Partial) => { + const { isCloudEnabled } = this.context; + + // when cloudUrl changes, fill proxy address and server name + const { cloudUrl } = changedFields; + if (cloudUrl) { + const { proxyAddress, serverName } = convertCloudUrlToProxyConnection(cloudUrl); + changedFields = { + ...changedFields, + proxyAddress, + serverName, + }; + } + + this.setState(({ fields: prevFields }) => { + const newFields = { + ...prevFields, + ...changedFields, + }; + return { + fields: newFields, + fieldsErrors: validateCluster(newFields, isCloudEnabled), + }; + }); + }; + + getCluster(): Cluster { + const { + fields: { + name, + mode, + seeds, + nodeConnections, + proxyAddress, + proxySocketConnections, + serverName, + skipUnavailable, + }, + } = this.state; + const { cluster } = this.props; + + let modeSettings; + + if (mode === PROXY_MODE) { + modeSettings = { + proxyAddress, + proxySocketConnections, + serverName, + }; + } else { + modeSettings = { + seeds, + nodeConnections, + }; + } + + return { + name, + skipUnavailable, + mode, + hasDeprecatedProxySetting: cluster?.hasDeprecatedProxySetting, + ...modeSettings, + }; + } + + save = () => { + const { save } = this.props; + + if (this.hasErrors()) { + this.setState({ + areErrorsVisible: true, + }); + return; + } + + const cluster = this.getCluster(); + save(cluster); + }; + + onSkipUnavailableChange = (e: EuiSwitchEvent) => { + const skipUnavailable = e.target.checked; + this.onFieldsChange({ skipUnavailable }); + }; + + resetToDefault = (fieldName: keyof Cluster) => { + this.onFieldsChange({ + [fieldName]: defaultClusterValues[fieldName], + }); + }; + + hasErrors = () => { + const { fieldsErrors } = this.state; + const errorValues = Object.values(fieldsErrors); + return errorValues.some((error) => error != null); + }; + + renderSkipUnavailable() { + const { + fields: { skipUnavailable }, + } = this.state; + + return ( + +

    + +

    + + } + description={ + +

    + + + + ), + learnMoreLink: ( + + + + ), + }} + /> +

    +
    + } + fullWidth + > + { + this.resetToDefault('skipUnavailable'); + }} + > + + + ) : null + } + > + + +
    + ); + } + + renderActions() { + const { isSaving, cancel } = this.props; + const { areErrorsVisible, isRequestVisible } = this.state; + + if (isSaving) { + return ( + + + + + + + + + + + + ); + } + + let cancelButton; + + if (cancel) { + cancelButton = ( + + + + + + ); + } + + const isSaveDisabled = areErrorsVisible && this.hasErrors(); + + return ( + + + + + + + + + + {cancelButton} + + + + + + {isRequestVisible ? ( + + ) : ( + + )} + + + + ); + } + + renderSavingFeedback() { + if (this.props.isSaving) { + return ( + + + + ); + } + + return null; + } + + renderSaveErrorFeedback() { + const { saveError } = this.props; + + if (saveError) { + const { message, cause } = saveError; + + let errorBody; + + if (cause && Array.isArray(cause)) { + if (cause.length === 1) { + errorBody =

    {cause[0]}

    ; + } else { + errorBody = ( +
      + {cause.map((causeValue) => ( +
    • {causeValue}
    • + ))} +
    + ); + } + } + + return ( + + + {errorBody} + + + + + ); + } + + return null; + } + + renderErrors = () => { + const { + areErrorsVisible, + fieldsErrors: { + name: errorClusterName, + seeds: errorsSeeds, + proxyAddress: errorProxyAddress, + serverName: errorServerName, + cloudUrl: errorCloudUrl, + }, + } = this.state; + + const hasErrors = this.hasErrors(); + + if (!areErrorsVisible || !hasErrors) { + return null; + } + + const errorExplanations = []; + + if (errorClusterName) { + errorExplanations.push({ + key: 'nameExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', { + defaultMessage: 'The "Name" field is invalid.', + }), + error: errorClusterName, + }); + } + + if (errorsSeeds) { + errorExplanations.push({ + key: 'seedsExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', { + defaultMessage: 'The "Seed nodes" field is invalid.', + }), + error: errorsSeeds, + }); + } + + if (errorProxyAddress) { + errorExplanations.push({ + key: 'proxyAddressExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', { + defaultMessage: 'The "Proxy address" field is invalid.', + }), + error: errorProxyAddress, + }); + } + + if (errorServerName) { + errorExplanations.push({ + key: 'serverNameExplanation', + field: i18n.translate( + 'xpack.remoteClusters.remoteClusterForm.inputServerNameErrorMessage', + { + defaultMessage: 'The "Server name" field is invalid.', + } + ), + error: errorServerName, + }); + } + + if (errorCloudUrl) { + errorExplanations.push({ + key: 'cloudUrlExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputcloudUrlErrorMessage', { + defaultMessage: 'The "Elasticsearch endpoint URL" field is invalid.', + }), + error: errorCloudUrl, + }); + } + + const messagesToBeRendered = errorExplanations.length && ( + +
    + {errorExplanations.map(({ key, field, error }) => ( +
    +
    {field}
    +
    {error}
    +
    + ))} +
    +
    + ); + + return ( + + + + + + } + color="danger" + iconType="cross" + /> + {messagesToBeRendered} + + ); + }; + + render() { + const { isRequestVisible, areErrorsVisible, fields, fieldsErrors } = this.state; + const { name: errorClusterName } = fieldsErrors; + const { cluster } = this.props; + const isNew = !cluster; + return ( + + {this.renderSaveErrorFeedback()} + + + +

    + +

    + + } + description={ + + } + fullWidth + > + + } + helpText={ + + } + error={errorClusterName} + isInvalid={Boolean(areErrorsVisible && errorClusterName)} + fullWidth + > + this.onFieldsChange({ name: e.target.value })} + fullWidth + disabled={!isNew} + data-test-subj="remoteClusterFormNameInput" + /> + +
    + + + + {this.renderSkipUnavailable()} +
    + + {this.renderErrors()} + + + + {this.renderActions()} + + {this.renderSavingFeedback()} + + {isRequestVisible ? ( + this.setState({ isRequestVisible: false })} + /> + ) : null} +
    + ); + } +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx index 4e402b8b55a5b..2bcedc2bce458 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/request_flyout.tsx @@ -24,13 +24,13 @@ import { Cluster, serializeCluster } from '../../../../../common/lib'; interface Props { close: () => void; - name: string; cluster: Cluster; } export class RequestFlyout extends PureComponent { render() { - const { name, close, cluster } = this.props; + const { close, cluster } = this.props; + const { name } = cluster; const endpoint = 'PUT _cluster/settings'; const payload = JSON.stringify(serializeCluster(cluster), null, 2); const request = `${endpoint}\n${payload}`; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts index 67a5d8f727f3e..6f3956a19f6a0 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.ts @@ -10,3 +10,10 @@ export { validateProxy } from './validate_proxy'; export { validateSeeds } from './validate_seeds'; export { validateSeed } from './validate_seed'; export { validateServerName } from './validate_server_name'; +export { validateCluster, ClusterErrors } from './validate_cluster'; +export { + isCloudUrlEnabled, + validateCloudUrl, + convertProxyConnectionToCloudUrl, + convertCloudUrlToProxyConnection, +} from './validate_cloud_url'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts new file mode 100644 index 0000000000000..599706ba85b02 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + isCloudUrlEnabled, + validateCloudUrl, + convertCloudUrlToProxyConnection, + convertProxyConnectionToCloudUrl, + i18nTexts, +} from './validate_cloud_url'; + +describe('Cloud url', () => { + describe('validation', () => { + it('errors when the url is empty', () => { + const actual = validateCloudUrl(''); + expect(actual).toBe(i18nTexts.urlEmpty); + }); + + it('errors when the url is invalid', () => { + const actual = validateCloudUrl('invalid%url'); + expect(actual).toBe(i18nTexts.urlInvalid); + }); + }); + + describe('is cloud url', () => { + it('true for a new cluster', () => { + const actual = isCloudUrlEnabled(); + expect(actual).toBe(true); + }); + + it('true when proxy connection is empty', () => { + const actual = isCloudUrlEnabled({ name: 'test', proxyAddress: '', serverName: '' }); + expect(actual).toBe(true); + }); + + it('true when proxy address is the same as server name and default port', () => { + const actual = isCloudUrlEnabled({ + name: 'test', + proxyAddress: 'some-proxy:9400', + serverName: 'some-proxy', + }); + expect(actual).toBe(true); + }); + it('false when proxy address is the same as server name but not default port', () => { + const actual = isCloudUrlEnabled({ + name: 'test', + proxyAddress: 'some-proxy:1234', + serverName: 'some-proxy', + }); + expect(actual).toBe(false); + }); + it('true when proxy address is not the same as server name', () => { + const actual = isCloudUrlEnabled({ + name: 'test', + proxyAddress: 'some-proxy:9400', + serverName: 'some-server-name', + }); + expect(actual).toBe(false); + }); + }); + describe('conversion from cloud url', () => { + it('empty url to empty proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection(''); + expect(actual).toEqual({ proxyAddress: '', serverName: '' }); + }); + + it('url with protocol and port to proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('http://test.com:1234'); + expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' }); + }); + + it('url with protocol and no port to proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('http://test.com'); + expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' }); + }); + + it('url with no protocol to proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('test.com'); + expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' }); + }); + it('invalid url to empty proxy connection values', () => { + const actual = convertCloudUrlToProxyConnection('invalid%url'); + expect(actual).toEqual({ proxyAddress: '', serverName: '' }); + }); + }); + + describe('conversion to cloud url', () => { + it('empty proxy address to empty cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: '', + serverName: 'test', + }); + expect(actual).toEqual(''); + }); + + it('empty server name to empty cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: 'test', + serverName: '', + }); + expect(actual).toEqual(''); + }); + + it('different proxy address and server name to empty cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: 'test', + serverName: 'another-test', + }); + expect(actual).toEqual(''); + }); + + it('valid proxy connection to cloud url', () => { + const actual = convertProxyConnectionToCloudUrl({ + name: 'test', + proxyAddress: 'test-proxy:9400', + serverName: 'test-proxy', + }); + expect(actual).toEqual('test-proxy'); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx new file mode 100644 index 0000000000000..1f4862f0113e7 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cloud_url.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Cluster } from '../../../../../../common/lib'; +import { isAddressValid } from './validate_address'; + +export const i18nTexts = { + urlEmpty: ( + + ), + urlInvalid: ( + + ), +}; + +const CLOUD_DEFAULT_PROXY_PORT = '9400'; +const EMPTY_PROXY_VALUES = { proxyAddress: '', serverName: '' }; +const PROTOCOL_REGEX = new RegExp(/^https?:\/\//); + +export const isCloudUrlEnabled = (cluster?: Cluster): boolean => { + // enable cloud url for new clusters + if (!cluster) { + return true; + } + const { proxyAddress, serverName } = cluster; + if (!proxyAddress && !serverName) { + return true; + } + const portParts = (proxyAddress ?? '').split(':'); + const proxyAddressWithoutPort = portParts[0]; + const port = portParts[1]; + return port === CLOUD_DEFAULT_PROXY_PORT && proxyAddressWithoutPort === serverName; +}; + +const formatUrl = (url: string) => { + url = (url ?? '').trim().toLowerCase(); + // delete http(s):// protocol string if any + url = url.replace(PROTOCOL_REGEX, ''); + return url; +}; + +export const convertProxyConnectionToCloudUrl = (cluster?: Cluster): string => { + if (!isCloudUrlEnabled(cluster)) { + return ''; + } + return cluster?.serverName ?? ''; +}; +export const convertCloudUrlToProxyConnection = ( + cloudUrl: string = '' +): { proxyAddress: string; serverName: string } => { + cloudUrl = formatUrl(cloudUrl); + if (!cloudUrl || !isAddressValid(cloudUrl)) { + return EMPTY_PROXY_VALUES; + } + const address = cloudUrl.split(':')[0]; + return { proxyAddress: `${address}:${CLOUD_DEFAULT_PROXY_PORT}`, serverName: address }; +}; + +export const validateCloudUrl = (cloudUrl: string): JSX.Element | null => { + if (!cloudUrl) { + return i18nTexts.urlEmpty; + } + cloudUrl = formatUrl(cloudUrl); + if (!isAddressValid(cloudUrl)) { + return i18nTexts.urlInvalid; + } + return null; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx new file mode 100644 index 0000000000000..e0fa434f21d5c --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_cluster.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateName } from './validate_name'; +import { PROXY_MODE, SNIFF_MODE } from '../../../../../../common/constants'; +import { validateSeeds } from './validate_seeds'; +import { validateProxy } from './validate_proxy'; +import { validateServerName } from './validate_server_name'; +import { validateCloudUrl } from './validate_cloud_url'; +import { FormFields } from '../remote_cluster_form'; + +type ClusterError = JSX.Element | null; + +export interface ClusterErrors { + name?: ClusterError; + seeds?: ClusterError; + proxyAddress?: ClusterError; + serverName?: ClusterError; + cloudUrl?: ClusterError; +} +export const validateCluster = (fields: FormFields, isCloudEnabled: boolean): ClusterErrors => { + const { name, seeds = [], mode, proxyAddress, serverName, cloudUrlEnabled, cloudUrl } = fields; + + return { + name: validateName(name), + seeds: mode === SNIFF_MODE ? validateSeeds(seeds) : null, + proxyAddress: !cloudUrlEnabled && mode === PROXY_MODE ? validateProxy(proxyAddress) : null, + // server name is only required in cloud when proxy mode is enabled + serverName: + !cloudUrlEnabled && isCloudEnabled && mode === PROXY_MODE + ? validateServerName(serverName) + : null, + cloudUrl: cloudUrlEnabled ? validateCloudUrl(cloudUrl) : null, + }; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts deleted file mode 100644 index a5b3656b36de5..0000000000000 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.ts +++ /dev/null @@ -1,43 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -import { isAddressValid, isPortValid } from './validate_address'; - -export function validateSeed(seed?: string): string[] { - const errors: string[] = []; - - if (!seed) { - return errors; - } - - const isValid = isAddressValid(seed); - - if (!isValid) { - errors.push( - i18n.translate( - 'xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage', - { - defaultMessage: - 'Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. ' + - 'Hosts can only consist of letters, numbers, and dashes.', - } - ) - ); - } - - if (!isPortValid(seed)) { - errors.push( - i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage', { - defaultMessage: 'A port is required.', - }) - ); - } - - return errors; -} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.tsx new file mode 100644 index 0000000000000..4863dff5ec337 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { isAddressValid, isPortValid } from './validate_address'; + +export function validateSeed(seed?: string): JSX.Element[] { + const errors: JSX.Element[] = []; + + if (!seed) { + return errors; + } + + const isValid = isAddressValid(seed); + + if (!isValid) { + errors.push( + + ); + } + + if (!isPortValid(seed)) { + errors.push( + + ); + } + + return errors; +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/index.d.ts b/x-pack/plugins/remote_clusters/public/application/sections/index.d.ts new file mode 100644 index 0000000000000..ab0f579c1a415 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/index.d.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ComponentType } from 'react'; + +export declare const RemoteClusterEdit: ComponentType; +export declare const RemoteClusterAdd: ComponentType; +export declare const RemoteClusterList: ComponentType; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index 6ee6bd6d87d58..124d2d42afb78 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -73,7 +73,7 @@ export class RemoteClusterAdd extends PureComponent { description={ } /> diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index c68dd7ab10aa7..18ee2e2b3875d 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -27,10 +27,6 @@ import { getRouter, redirect } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components'; -const disabledFields = { - name: true, -}; - export class RemoteClusterEdit extends Component { static propTypes = { isLoading: PropTypes.bool, @@ -202,8 +198,7 @@ export class RemoteClusterEdit extends Component { ) : null} Store; diff --git a/x-pack/plugins/remote_clusters/public/assets/cloud_screenshot.png b/x-pack/plugins/remote_clusters/public/assets/cloud_screenshot.png new file mode 100644 index 0000000000000..f6c9302ef76ea Binary files /dev/null and b/x-pack/plugins/remote_clusters/public/assets/cloud_screenshot.png differ diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index 107d4e127d1b5..540a8b40a6208 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -60,13 +60,14 @@ export class RemoteClustersUIPlugin initNotification(toasts, fatalErrors); initHttp(http); - const isCloudEnabled = Boolean(cloud?.isCloudEnabled); + const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled); + const cloudBaseUrl: string = cloud?.baseUrl ?? ''; const { renderApp } = await import('./application'); const unmountAppCallback = await renderApp( element, i18nContext, - { isCloudEnabled }, + { isCloudEnabled, cloudBaseUrl }, history ); diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json index b48933bc9f1ec..9dc7926bd62ea 100644 --- a/x-pack/plugins/remote_clusters/tsconfig.json +++ b/x-pack/plugins/remote_clusters/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -8,10 +8,12 @@ "declarationMap": true }, "include": [ + "__jest__/**/*", "common/**/*", "fixtures/**/*", "public/**/*", "server/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 4f252743ed078..88e8d343f4700 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts deleted file mode 100644 index 0296428c49613..0000000000000 --- a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts +++ /dev/null @@ -1,142 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.rollup = components.clientAction.namespaceFactory(); - const rollup = Client.prototype.rollup.prototype; - - rollup.rollupIndexCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_rollup/data', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.search = ca({ - urls: [ - { - fmt: '/<%=index%>/_rollup_search', - req: { - index: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - rollup.fieldCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_field_caps?fields=*', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.jobs = ca({ - urls: [ - { - fmt: '/_rollup/job/_all', - }, - ], - method: 'GET', - }); - - rollup.job = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.startJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>/_start', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - rollup.stopJob = ca({ - params: { - waitForCompletion: { - type: 'boolean', - name: 'wait_for_completion', - }, - }, - urls: [ - { - fmt: '/_rollup/job/<%=id%>/_stop', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - rollup.deleteJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - rollup.createJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); -}; diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 1b982ab45205d..ff6adc1c8d24b 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -19,25 +19,16 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { PLUGIN, CONFIG_ROLLUPS } from '../common'; -import { Dependencies, RollupHandlerContext } from './types'; +import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; import { registerRollupUsageCollector } from './collectors'; import { rollupDataEnricher } from './rollup_data_enricher'; import { IndexPatternsFetcher } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; import { getCapabilitiesForRollupIndices } from '../../../../src/plugins/data/server'; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - // Extend the elasticsearchJs client with additional endpoints. - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - - return core.elasticsearch.legacy.createClient('rollup', esClientConfig); -} - export class RollupPlugin implements Plugin { private readonly logger: Logger; private readonly globalConfig$: Observable; @@ -82,21 +73,11 @@ export class RollupPlugin implements Plugin { ], }); - http.registerRouteHandlerContext( - 'rollup', - async (context, request) => { - this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.rollupEsClient.asScoped(request), - }; - } - ); - registerApiRoutes({ router: http.createRouter(), license: this.license, lib: { - isEsError, + handleEsError, formatEsError, getCapabilitiesForRollupIndices, }, diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts index 694ab3c467c1f..1d3be4b8e1fbb 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -14,7 +14,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices }, + lib: { handleEsError, getCapabilitiesForRollupIndices }, }: RouteDependencies) => { router.get( { @@ -23,18 +23,13 @@ export const registerGetRoute = ({ }, license.guardApiRoute(async (context, request, response) => { try { - const data = await context.rollup!.client.callAsCurrentUser( - 'rollup.rollupIndexCapabilities', - { - indexPattern: '_all', - } - ); + const { client: clusterClient } = context.core.elasticsearch; + const { body: data } = await clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ + index: '_all', + }); return response.ok({ body: getCapabilitiesForRollupIndices(data) }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts index 90eabaa88b641..b2431c3838234 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -32,10 +32,6 @@ interface FieldCapability { scaled_float?: any; } -interface FieldCapabilities { - fields: FieldCapability[]; -} - function isNumericField(fieldCapability: FieldCapability) { const numericTypes = [ 'long', @@ -59,7 +55,7 @@ function isNumericField(fieldCapability: FieldCapability) { export const registerValidateIndexPatternRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -71,16 +67,12 @@ export const registerValidateIndexPatternRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { indexPattern } = request.params; - const [fieldCapabilities, rollupIndexCapabilities]: [ - FieldCapabilities, - { [key: string]: any } - ] = await Promise.all([ - context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }), - context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { - indexPattern, - }), + const [{ body: fieldCapabilities }, { body: rollupIndexCapabilities }] = await Promise.all([ + clusterClient.asCurrentUser.fieldCaps({ index: indexPattern, fields: '*' }), + clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ index: indexPattern }), ]); const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; @@ -92,23 +84,21 @@ export const registerValidateIndexPatternRoute = ({ const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); - fieldCapabilitiesEntries.forEach( - ([fieldName, fieldCapability]: [string, FieldCapability]) => { - if (fieldCapability.date) { - dateFields.push(fieldName); - return; - } + fieldCapabilitiesEntries.forEach(([fieldName, fieldCapability]) => { + if (fieldCapability.date) { + dateFields.push(fieldName); + return; + } - if (isNumericField(fieldCapability)) { - numericFields.push(fieldName); - return; - } + if (isNumericField(fieldCapability)) { + numericFields.push(fieldName); + return; + } - if (fieldCapability.keyword) { - keywordFields.push(fieldName); - } + if (fieldCapability.keyword) { + keywordFields.push(fieldName); } - ); + }); const body = { doesMatchIndices, @@ -132,11 +122,7 @@ export const registerValidateIndexPatternRoute = ({ return response.ok({ body: notFoundBody }); } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts index bcb3a337aa725..11cfaf8851d45 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerCreateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.put( { @@ -29,21 +29,19 @@ export const registerCreateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { id, ...rest } = request.body.job; // Create job. - await context.rollup!.client.callAsCurrentUser('rollup.createJob', { + await clusterClient.asCurrentUser.rollup.putJob({ id, body: rest, }); // Then request the newly created job. - const results = await context.rollup!.client.callAsCurrentUser('rollup.job', { id }); + const { body: results } = await clusterClient.asCurrentUser.rollup.getJobs({ id }); return response.ok({ body: results.jobs[0] }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts index 4bbe73753e96c..f90a81f73823e 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerDeleteRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -24,28 +24,29 @@ export const registerDeleteRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; const data = await Promise.all( - jobIds.map((id: string) => - context.rollup!.client.callAsCurrentUser('rollup.deleteJob', { id }) - ) + jobIds.map((id: string) => clusterClient.asCurrentUser.rollup.deleteJob({ id })) ).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { // There is an issue opened on ES to handle the following error correctly // https://github.com/elastic/elasticsearch/issues/42908 // Until then we'll modify the response here. - if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { - err.status = 400; - err.statusCode = 400; - err.displayName = 'Bad request'; - err.message = JSON.parse(err.response).task_failures[0].reason.reason; - } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); + if ( + err?.meta && + err.body?.task_failures[0]?.reason?.reason?.includes( + 'Job must be [STOPPED] before deletion' + ) + ) { + err.meta.status = 400; + err.meta.statusCode = 400; + err.meta.displayName = 'Bad request'; + err.message = err.body.task_failures[0].reason.reason; } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts index a9a30c0370c5f..9944df2e55919 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts @@ -11,7 +11,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -19,14 +19,12 @@ export const registerGetRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { - const data = await context.rollup!.client.callAsCurrentUser('rollup.jobs'); + const { body: data } = await clusterClient.asCurrentUser.rollup.getJobs({ id: '_all' }); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts index 2ebfcc437f41e..133c0cb34c9f5 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerStartRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -29,20 +29,16 @@ export const registerStartRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; const data = await Promise.all( - jobIds.map((id: string) => - context.rollup!.client.callAsCurrentUser('rollup.startJob', { id }) - ) + jobIds.map((id: string) => clusterClient.asCurrentUser.rollup.startJob({ id })) ).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts index faaf377a2d833..164273f604b43 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerStopRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -27,23 +27,21 @@ export const registerStopRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; // For our API integration tests we need to wait for the jobs to be stopped // in order to be able to delete them sequentially. const { waitForCompletion } = request.query; const stopRollupJob = (id: string) => - context.rollup!.client.callAsCurrentUser('rollup.stopJob', { + clusterClient.asCurrentUser.rollup.stopJob({ id, - waitForCompletion: waitForCompletion === 'true', + wait_for_completion: waitForCompletion === 'true', }); const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts index f77ae7829bb6c..62aec4e01eaa0 100644 --- a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerSearchRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -27,21 +27,21 @@ export const registerSearchRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const requests = request.body.map(({ index, query }: { index: string; query?: any }) => - context.rollup.client.callAsCurrentUser('rollup.search', { - index, - rest_total_hits_as_int: true, - body: query, - }) + clusterClient.asCurrentUser.rollup + .rollupSearch({ + index, + rest_total_hits_as_int: true, + body: query, + }) + .then(({ body }) => body) ); const data = await Promise.all(requests); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/services/license.ts b/x-pack/plugins/rollup/server/services/license.ts index d2c3ff82eab1c..1b88a4020afa6 100644 --- a/x-pack/plugins/rollup/server/services/license.ts +++ b/x-pack/plugins/rollup/server/services/license.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { Logger } from 'src/core/server'; +import { Logger, RequestHandlerContext } from 'src/core/server'; import { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'src/core/server'; import { LicensingPluginSetup } from '../../../licensing/server'; import { LicenseType } from '../../../licensing/common/types'; -import type { RollupHandlerContext } from '../types'; export interface LicenseStatus { isValid: boolean; @@ -57,11 +56,11 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( - ctx: RollupHandlerContext, + ctx: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory ) { diff --git a/x-pack/plugins/rollup/server/shared_imports.ts b/x-pack/plugins/rollup/server/shared_imports.ts index 2167558c39652..fe157644c6b3d 100644 --- a/x-pack/plugins/rollup/server/shared_imports.ts +++ b/x-pack/plugins/rollup/server/shared_imports.ts @@ -7,4 +7,4 @@ export { IndexPatternsFetcher } from '../../../../src/plugins/data/server'; -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 45dcc976b211f..c774644da46ce 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; +import { IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; @@ -15,7 +15,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; export interface Dependencies { @@ -27,10 +27,10 @@ export interface Dependencies { } export interface RouteDependencies { - router: RollupPluginRouter; + router: IRouter; license: License; lib: { - isEsError: typeof isEsError; + handleEsError: typeof handleEsError; formatEsError: typeof formatEsError; getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; }; @@ -38,22 +38,3 @@ export interface RouteDependencies { IndexPatternsFetcher: typeof IndexPatternsFetcher; }; } - -/** - * @internal - */ -interface RollupApiRequestHandlerContext { - client: ILegacyScopedClusterClient; -} - -/** - * @internal - */ -export interface RollupHandlerContext extends RequestHandlerContext { - rollup: RollupApiRequestHandlerContext; -} - -/** - * @internal - */ -export type RollupPluginRouter = IRouter; diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json index bf589c62713d6..9b994d1710ffc 100644 --- a/x-pack/plugins/rollup/tsconfig.json +++ b/x-pack/plugins/rollup/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/runtime_fields/tsconfig.json b/x-pack/plugins/runtime_fields/tsconfig.json index e1ad141f1c702..a1efe4c9cf2dd 100644 --- a/x-pack/plugins/runtime_fields/tsconfig.json +++ b/x-pack/plugins/runtime_fields/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/saved_objects_tagging/tsconfig.json b/x-pack/plugins/saved_objects_tagging/tsconfig.json index 5c37481f982d9..d00156ad1277c 100644 --- a/x-pack/plugins/saved_objects_tagging/tsconfig.json +++ b/x-pack/plugins/saved_objects_tagging/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json index 57cd882422b39..f8ac3a61f7812 100644 --- a/x-pack/plugins/searchprofiler/tsconfig.json +++ b/x-pack/plugins/searchprofiler/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 3002db642bc1e..5df73f7f8ec4e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -480,23 +480,25 @@ describe('', () => { }); }); - it('can render if features are not available', async () => { - const { http } = coreMock.createStart(); - http.get.mockImplementation(async (path: any) => { - if (path === '/api/features') { - const error = { response: { status: 404 } }; - throw error; - } + it('registers fatal error if features endpoint fails unexpectedly', async () => { + const error = { response: { status: 500 } }; + const getFeatures = jest.fn().mockRejectedValue(error); + const props = getProps({ action: 'edit' }); + const wrapper = mountWithIntl(); - if (path === '/api/spaces/space') { - return buildSpaces(); - } - }); + await waitForRender(wrapper); + expect(props.fatalErrors.add).toHaveBeenLastCalledWith(error); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(0); + }); - const wrapper = mountWithIntl(); + it('can render if features call is not allowed', async () => { + const error = { response: { status: 403 } }; + const getFeatures = jest.fn().mockRejectedValue(error); + const props = getProps({ action: 'edit' }); + const wrapper = mountWithIntl(); await waitForRender(wrapper); - + expect(props.fatalErrors.add).not.toHaveBeenCalled(); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); expectSaveFormButtons(wrapper); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 5d6b4a1b4fdaf..f810cd2079d16 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -256,13 +256,12 @@ function useFeatures( // possible that a user with `manage_security` will attempt to visit the role management page without the // correct Kibana privileges. If that's the case, then they receive a partial view of the role, and the UI does // not allow them to make changes to that role's kibana privileges. When this user visits the edit role page, - // this API endpoint will throw a 404, which causes view to fail completely. So we instead attempt to detect the - // 404 here, and respond in a way that still allows the UI to render itself. - const unauthorizedForFeatures = err.response?.status === 404; + // this API endpoint will throw a 403, which causes view to fail completely. So we instead attempt to detect the + // 403 here, and respond in a way that still allows the UI to render itself. + const unauthorizedForFeatures = err.response?.status === 403; if (unauthorizedForFeatures) { return [] as KibanaFeature[]; } - fatalErrors.add(err); }) .then((retrievedFeatures) => { @@ -296,7 +295,6 @@ export const EditRolePage: FunctionComponent = ({ // We should keep the same mutable instance of Validator for every re-render since we'll // eventually enable validation after the first time user tries to save a role. const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); - const [formError, setFormError] = useState(null); const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx index 8433f54a73343..29d87e31797cc 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -262,6 +262,7 @@ export const UserForm: FunctionComponent = ({ > = ({ > = ({ > = ({ > = ({ > { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}>"', + 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', ]); expect(message.schema).toEqual({}); }); @@ -125,8 +125,8 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"', - 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts new file mode 100644 index 0000000000000..c0888a6c2a4bd --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import seedrandom from 'seedrandom'; +import uuid from 'uuid'; + +const OS_FAMILY = ['windows', 'macos', 'linux']; + +/** + * A generic base class to assist in creating domain specific data generators. It includes + * several general purpose random data generators for use within the class and exposes one + * public method named `generate()` which should be implemented by sub-classes. + */ +export class BaseDataGenerator { + protected random: seedrandom.prng; + + constructor(seed: string | seedrandom.prng = Math.random().toString()) { + if (typeof seed === 'string') { + this.random = seedrandom(seed); + } else { + this.random = seed; + } + } + + /** + * Generate a new record + */ + public generate(): GeneratedDoc { + throw new Error('method not implemented!'); + } + + /** generate random OS family value */ + protected randomOSFamily(): string { + return this.randomChoice(OS_FAMILY); + } + + /** generate a UUID (v4) */ + protected randomUUID(): string { + return uuid.v4(); + } + + /** Generate a random number up to the max provided */ + protected randomN(max: number): number { + return Math.floor(this.random() * max); + } + + protected *randomNGenerator(max: number, count: number) { + let iCount = count; + while (iCount > 0) { + yield this.randomN(max); + iCount = iCount - 1; + } + } + + /** + * Create an array of a given size and fill it with data provided by a generator + * + * @param lengthLimit + * @param generator + * @protected + */ + protected randomArray(lengthLimit: number, generator: () => T): T[] { + const rand = this.randomN(lengthLimit) + 1; + return [...Array(rand).keys()].map(generator); + } + + protected randomMac(): string { + return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-'); + } + + protected randomIP(): string { + return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.'); + } + + protected randomVersion(): string { + return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); + } + + protected randomChoice(choices: T[]): T { + return choices[this.randomN(choices.length)]; + } + + protected randomString(length: number): string { + return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join(''); + } + + protected randomHostname(): string { + return `Host-${this.randomString(10)}`; + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts new file mode 100644 index 0000000000000..6bdbb9cde2034 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseDataGenerator } from './base_data_generator'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../lists/common/constants'; +import { CreateExceptionListItemSchema } from '../../../../lists/common'; +import { getCreateExceptionListItemSchemaMock } from '../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; + +export class EventFilterGenerator extends BaseDataGenerator { + generate(): CreateExceptionListItemSchema { + const overrides: Partial = { + name: `generator event ${this.randomString(5)}`, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + item_id: `generator_endpoint_event_filter_${this.randomUUID()}`, + os_types: [this.randomOSFamily()] as CreateExceptionListItemSchema['os_types'], + tags: ['policy:all'], + namespace_type: 'agnostic', + meta: undefined, + }; + + return Object.assign>( + getCreateExceptionListItemSchemaMock(), + overrides + ); + } +} 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 8aec9768dd50d..36d0b0cbf3b21 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -35,6 +35,7 @@ import { EsAssetReference, KibanaAssetReference } from '../../../fleet/common/ty import { agentPolicyStatuses } from '../../../fleet/common/constants'; import { firstNonNullValue } from './models/ecs_safety_helpers'; import { EventOptions } from './types/generator'; +import { BaseDataGenerator } from './data_generators/base_data_generator'; export type Event = AlertEvent | SafeEndpointEvent; /** @@ -386,9 +387,8 @@ const alertsDefaultDataStream = { namespace: 'default', }; -export class EndpointDocGenerator { +export class EndpointDocGenerator extends BaseDataGenerator { commonInfo: HostInfo; - random: seedrandom.prng; sequence: number = 0; /** * The EndpointDocGenerator parameters @@ -396,12 +396,7 @@ export class EndpointDocGenerator { * @param seed either a string to seed the random number generator or a random number generator function */ constructor(seed: string | seedrandom.prng = Math.random().toString()) { - if (typeof seed === 'string') { - this.random = seedrandom(seed); - } else { - this.random = seed; - } - + super(seed); this.commonInfo = this.createHostData(); } @@ -1568,47 +1563,6 @@ export class EndpointDocGenerator { }; } - private randomN(n: number): number { - return Math.floor(this.random() * n); - } - - private *randomNGenerator(max: number, count: number) { - let iCount = count; - while (iCount > 0) { - yield this.randomN(max); - iCount = iCount - 1; - } - } - - private randomArray(lengthLimit: number, generator: () => T): T[] { - const rand = this.randomN(lengthLimit) + 1; - return [...Array(rand).keys()].map(generator); - } - - private randomMac(): string { - return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-'); - } - - public randomIP(): string { - return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.'); - } - - private randomVersion(): string { - return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); - } - - private randomChoice(choices: T[]): T { - return choices[this.randomN(choices.length)]; - } - - private randomString(length: number): string { - return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join(''); - } - - private randomHostname(): string { - return `Host-${this.randomString(10)}`; - } - private seededUUIDv4(): string { return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); } @@ -1646,6 +1600,10 @@ export class EndpointDocGenerator { private randomProcessName(): string { return this.randomChoice(fakeProcessNames); } + + public randomIP(): string { + return super.randomIP(); + } } const fakeProcessNames = [ diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 19de81cb95c3f..39551e3ee6f1c 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -14,6 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const allowedExperimentalValues = Object.freeze({ fleetServerEnabled: false, trustedAppsByPolicyEnabled: false, + eventFilteringEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts index 24fb5f1a95cd2..974e892a7312f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts @@ -8,7 +8,7 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { HostItem, HostsFields } from '../common'; -import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { CursorType, Direction, Inspect, Maybe, PageInfoPaginated } from '../../../common'; import { RequestOptionsPaginated } from '../..'; export interface HostsEdges { @@ -26,3 +26,9 @@ export interface HostsStrategyResponse extends IEsSearchResponse { export interface HostsRequestOptions extends RequestOptionsPaginated { defaultIndex: string[]; } + +export interface HostsSortField { + field: HostsFields; + + direction: Direction; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 11dc8ee2f6a82..a579d8f8d8ef3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -32,7 +32,7 @@ export interface HostItem { cloud?: Maybe; endpoint?: Maybe; host?: Maybe; - lastSeen?: Maybe; + lastSeen?: Maybe; } export interface HostValue { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/details/index.ts index 485d41895a4a6..34b32115a089d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/details/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { Inspect, Maybe, TimerangeInput } from '../../../common'; @@ -22,3 +23,7 @@ export interface HostDetailsRequestOptions extends Partial; } + +export interface AggregationRequest { + [aggField: string]: estypes.AggregationContainer; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts index b3e7b14aed000..df95f859e3f37 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts @@ -6,14 +6,14 @@ */ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { Inspect, Maybe } from '../../../common'; +import { Inspect, Maybe, Direction } from '../../../common'; import { RequestOptionsPaginated } from '../..'; import { HostsFields } from '../common'; export interface HostFirstLastSeenRequestOptions extends Partial> { hostName: string; - order: 'asc' | 'desc'; + order: Direction.asc | Direction.desc; } export interface HostFirstLastSeenStrategyResponse extends IEsSearchResponse { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index fa3029405dc22..3926fdc72f73a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -16,7 +16,7 @@ export * from './uncommon_processes'; export enum HostsQueries { authentications = 'authentications', - details = 'details', + details = 'hostDetails', firstOrLastSeen = 'firstOrLastSeen', hosts = 'hosts', overview = 'overviewHost', diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index e9d17a361d336..b7c0e1c6fcd6e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -25,7 +25,7 @@ import { waitForAlerts, waitForAlertsIndexToBeCreated, } from '../../tasks/alerts'; -import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; +import { createCustomRuleActivated, deleteCustomRule } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -42,6 +42,7 @@ describe('Closing alerts', () => { createCustomRuleActivated(newRule); refreshPage(); waitForAlertsToPopulate(); + deleteCustomRule(); }); it('Closes and opens alerts', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index d290773d425e2..fb0a01bd1c7d3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -14,11 +14,7 @@ import { SHOWING_RULES_TEXT, } from '../../screens/alerts_detection_rules'; -import { - goToManageAlertsDetectionRules, - waitForAlertsIndexToBeCreated, - waitForAlertsPanelToBeLoaded, -} from '../../tasks/alerts'; +import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { changeRowsPerPageTo300, deleteFirstRule, @@ -47,7 +43,6 @@ describe('Alerts rules, prebuilt rules', () => { const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); @@ -79,7 +74,6 @@ describe('Deleting prebuilt rules', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 10644e046a68b..d66b839267ea0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -191,7 +191,10 @@ export const resetAllRulesIdleModalTimeout = () => { export const changeRowsPerPageTo = (rowsCount: number) => { cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); - cy.get(rowsPerPageSelector(rowsCount)).click(); + cy.get(rowsPerPageSelector(rowsCount)) + .pipe(($el) => $el.trigger('click')) + .should('not.be.visible'); + waitForRulesTableToBeRefreshed(); }; diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index bd8d9aa058bc3..270d877a362a6 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": [], "include": [ "./**/*" diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/jest.config.js index b4dcedfcceeee..700eaebf6c202 100644 --- a/x-pack/plugins/security_solution/jest.config.js +++ b/x-pack/plugins/security_solution/jest.config.js @@ -9,7 +9,4 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/security_solution'], - - // TODO: migrate to "jest-environment-jsdom" https://github.com/elastic/kibana/issues/95201 - testEnvironment: 'jest-environment-jsdom-thirteen', }; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 0fafdaf81f095..3ac0084e96fb3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -304,13 +304,15 @@ describe('AllCases', () => { ); + + wrapper + .find( + '[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]' + ) + .last() + .simulate('click'); + await waitFor(() => { - wrapper - .find( - '[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]' - ) - .last() - .simulate('click'); expect(wrapper.find('[data-test-subj="action-open"]').first().props().disabled).toEqual(true); expect( wrapper.find('[data-test-subj="action-in-progress"]').first().props().disabled @@ -347,8 +349,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click'); expect(setQueryParams).toBeCalledWith({ page: 1, perPage: 5, @@ -364,9 +366,10 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ caseId: firstCase.id, @@ -398,9 +401,11 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ caseId: firstCase.id, @@ -418,9 +423,11 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ caseId: firstCase.id, @@ -454,17 +461,20 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); + + wrapper + .find( + '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' + ) + .last() + .simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); expect(handleToggleModal).toBeCalled(); - wrapper - .find( - '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' - ) - .last() - .simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([ ...useGetCasesMockState.data.cases.map(({ id, type, title }) => ({ id, type, title })), { @@ -488,8 +498,10 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false); expect(wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').exists()).toEqual( false @@ -529,8 +541,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false); expect( wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().props().disabled @@ -556,9 +568,10 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click'); expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.closed); }); }); @@ -578,9 +591,9 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click'); expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.open); }); }); @@ -597,9 +610,9 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click'); expect(updateBulkStatus).toBeCalledWith( useGetCasesMockState.data.cases, CaseStatuses['in-progress'] @@ -695,8 +708,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); expect(onRowClick).toHaveBeenCalled(); }); }); @@ -716,8 +729,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); }); }); @@ -728,8 +741,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); expect(onRowClick).toHaveBeenCalledWith({ closedAt: null, closedBy: null, @@ -783,8 +796,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); expect(onRowClick).not.toHaveBeenCalled(); }); }); @@ -795,10 +808,9 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click'); expect(setQueryParams).toBeCalledWith({ sortField: 'closedAt', }); @@ -811,10 +823,9 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click'); expect(setQueryParams).toBeCalledWith({ sortField: 'updatedAt', }); @@ -827,10 +838,9 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click'); expect(setQueryParams).toBeCalledWith({ sortField: 'createdAt', }); @@ -843,9 +853,8 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').text()).toBe( 'Open (20)' ); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index f28c7791d0110..0daa62bf735e8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -22,7 +22,7 @@ import { TestProviders } from '../../../common/mock'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; -import { act, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; @@ -139,7 +139,6 @@ describe('CaseView ', () => { }; beforeEach(() => { - jest.clearAllMocks(); jest.resetAllMocks(); useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); @@ -241,17 +240,15 @@ describe('CaseView ', () => { ); + wrapper.find('[data-test-subj="case-view-status-dropdown"] button').first().simulate('click'); + wrapper + .find('button[data-test-subj="case-view-status-dropdown-closed"]') + .first() + .simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-view-status-dropdown"] button').first().simulate('click'); - wrapper.update(); - wrapper - .find('button[data-test-subj="case-view-status-dropdown-closed"]') - .first() - .simulate('click'); - - wrapper.update(); const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateCaseProperty).toHaveBeenCalledTimes(1); expect(updateObject.updateKey).toEqual('status'); expect(updateObject.updateValue).toEqual('closed'); }); @@ -572,36 +569,29 @@ describe('CaseView ', () => { ); - await waitFor(() => { - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); - }); + wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); await waitFor(() => { - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - wrapper.update(); }); - act(() => { - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - }); + wrapper.find(`button[data-test-subj="edit-connectors-submit"]`).first().simulate('click'); await waitFor(() => { wrapper.update(); - }); - - const updateObject = updateCaseProperty.mock.calls[0][0]; - expect(updateObject.updateKey).toEqual('connector'); - expect(updateObject.updateValue).toEqual({ - id: 'resilient-2', - name: 'My Connector 2', - type: ConnectorTypes.resilient, - fields: { - incidentTypes: null, - severityCode: null, - }, + const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateCaseProperty).toHaveBeenCalledTimes(1); + expect(updateObject.updateKey).toEqual('connector'); + expect(updateObject.updateValue).toEqual({ + id: 'resilient-2', + name: 'My Connector 2', + type: ConnectorTypes.resilient, + fields: { + incidentTypes: null, + severityCode: null, + }, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index c99cabb50e3dc..40a202f5257a7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -341,7 +341,7 @@ describe('AddToCaseAction', () => { ); expect( - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled') + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled') ).toBeTruthy(); }); @@ -358,7 +358,7 @@ describe('AddToCaseAction', () => { ); expect( - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled') + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled') ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index decd37a7646e7..45c1355cecfa7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -172,7 +172,7 @@ const AddToCaseActionComponent: React.FC = ({ size="s" iconType="folderClosed" onClick={openPopover} - disabled={isDisabled} + isDisabled={isDisabled} /> ), diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index 056add32add82..a5c6b2d50f4a2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -40,7 +40,8 @@ jest.mock('../../containers/use_update_comment'); jest.mock('./user_action_timestamp'); const patchComment = jest.fn(); -describe('UserActionTree ', () => { +// FLAKY: https://github.com/elastic/kibana/issues/96362 +describe.skip('UserActionTree ', () => { const sampleData = { content: 'what a great comment update', }; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index af90d17fe62b8..43d5c66655808 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -12,6 +12,8 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; import { SourcererScopeName } from '../../store/sourcerer/model'; @@ -91,6 +93,8 @@ const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={endDate} id={timelineId} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} /> 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 3ecc17589fe08..8962f5e6c5146 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 @@ -26,6 +26,8 @@ import { KqlMode } from '../../../timelines/store/timeline/model'; import { SortDirection } from '../../../timelines/components/timeline/body/sort'; import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; jest.mock('../../../timelines/components/graph_overlay', () => ({ @@ -99,6 +101,8 @@ const eventsViewerDefaultProps = { query: '', language: 'kql', }, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, sort: [ { @@ -118,6 +122,8 @@ describe('EventsViewer', () => { defaultModel: eventsDefaultModel, end: to, id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, scopeId: SourcererScopeName.timeline, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 050cd92b0556e..e6e868f1a7365 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; @@ -41,7 +40,9 @@ import { useManageTimeline } from '../../../timelines/components/manage_timeline import { ExitFullScreen } from '../exit_full_screen'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px @@ -122,6 +123,8 @@ interface Props { kqlMode: KqlMode; query: Query; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; start: string; sort: Sort[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -146,8 +149,10 @@ const EventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, sort, utilityBar, @@ -310,6 +315,8 @@ const EventsViewerComponent: React.FC = ({ isEventViewer={true} onRuleChange={onRuleChange} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ @@ -343,6 +350,7 @@ const EventsViewerComponent: React.FC = ({ export const EventsViewer = React.memo( EventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && @@ -359,6 +367,8 @@ export const EventsViewer = React.memo( prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.start === nextProps.start && deepEqual(prevProps.sort, nextProps.sort) && prevProps.utilityBar === nextProps.utilityBar && 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 5004c23f9111c..cd27177643b44 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 @@ -18,7 +18,9 @@ import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; jest.mock('../../../timelines/containers', () => ({ useTimelineEvents: jest.fn(), @@ -38,6 +40,8 @@ const testProps = { end: to, indexNames: [], id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, scopeId: SourcererScopeName.default, start: from, }; 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 59dc756bb2b3e..b58aa2236d292 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 @@ -22,6 +22,8 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { DetailsPanel } from '../../../timelines/components/side_panel'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; @@ -41,6 +43,8 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; } @@ -67,8 +71,10 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPageOptions, kqlMode, pageFilters, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, scopeId, showCheckboxes, @@ -129,6 +135,8 @@ const StatefulEventsViewerComponent: React.FC = ({ kqlMode={kqlMode} query={query} onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} start={start} sort={sort} utilityBar={utilityBar} @@ -201,6 +209,7 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && prevProps.scopeId === nextProps.scopeId && @@ -215,6 +224,8 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 456dabec06c24..686acbe4ef321 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -12,14 +12,13 @@ import { waitFor } from '@testing-library/react'; import { AddExceptionModal } from './'; import { useCurrentUser } from '../../../../common/lib/kibana'; -import { useAsync } from '../../../../shared_imports'; +import { useAsync, ExceptionBuilder } from '../../../../shared_imports'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { useFetchIndex } from '../../../containers/source'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import * as builder from '../builder'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; @@ -49,7 +48,6 @@ jest.mock('../../../containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); -jest.mock('../builder'); jest.mock('../../../../shared_imports'); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); @@ -59,12 +57,12 @@ describe('When the add exception modal is opened', () => { ReturnType >; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest - .spyOn(builder, 'ExceptionBuilderComponent') + .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') .mockReturnValue(<>); (useAsync as jest.Mock).mockImplementation(() => ({ 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 7e9e7c40258da..07dcb2272748f 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 @@ -23,19 +23,23 @@ import { EuiText, EuiCallOut, } from '@elastic/eui'; -import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils'; +import { + hasEqlSequenceQuery, + isEqlRule, + isThresholdRule, +} from '../../../../../common/detection_engine/utils'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, ExceptionListType, -} from '../../../../../public/lists_plugin_deps'; + ExceptionBuilder, +} from '../../../../../public/shared_imports'; import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; @@ -50,6 +54,7 @@ import { entryHasListType, entryHasNonEcsType, retrieveAlertOsTypes, + filterIndexPatterns, } from '../helpers'; import { ErrorInfo, ErrorCallout } from '../error_callout'; import { AlertData, ExceptionsBuilderExceptionItem } from '../types'; @@ -393,13 +398,17 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} {i18n.EXCEPTION_BUILDER_INFO} - diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx deleted file mode 100644 index 2046ac46b8517..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ /dev/null @@ -1,69 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; - -import { filterIndexPatterns } from './helpers'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); - -const getMockIndexPattern = (): IIndexPattern => ({ - id: '1234', - title: 'logstash-*', - fields, -}); - -const mockEndpointFields = [ - { - name: 'file.path.caseless', - type: 'string', - esTypes: ['keyword'], - count: 0, - scripted: false, - searchable: true, - aggregatable: false, - readFromDocValues: false, - }, - { - name: 'file.Ext.code_signature.status', - type: 'string', - esTypes: ['text'], - count: 0, - scripted: false, - searchable: true, - aggregatable: false, - readFromDocValues: false, - subType: { nested: { path: 'file.Ext.code_signature' } }, - }, -]; - -export const getEndpointField = (name: string) => - mockEndpointFields.find((field) => field.name === name) as IFieldType; - -describe('Exception builder helpers', () => { - describe('#filterIndexPatterns', () => { - test('it returns index patterns without filtering if list type is "detection"', () => { - const mockIndexPatterns = getMockIndexPattern(); - const output = filterIndexPatterns(mockIndexPatterns, 'detection'); - - expect(output).toEqual(mockIndexPatterns); - }); - - test('it returns filtered index patterns if list type is "endpoint"', () => { - const mockIndexPatterns = { - ...getMockIndexPattern(), - fields: [...fields, ...mockEndpointFields], - }; - const output = filterIndexPatterns(mockIndexPatterns, 'endpoint'); - - expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx deleted file mode 100644 index 0ad9814484a2f..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ /dev/null @@ -1,43 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import uuid from 'uuid'; - -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { OperatorTypeEnum, ExceptionListType, OperatorEnum } from '../../../../lists_plugin_deps'; -import { ExceptionsBuilderExceptionItem, EmptyEntry, EmptyNestedEntry } from '../types'; -import exceptionableFields from '../exceptionable_fields.json'; - -export const filterIndexPatterns = ( - patterns: IIndexPattern, - type: ExceptionListType -): IIndexPattern => { - return type === 'endpoint' - ? { - ...patterns, - fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - } - : patterns; -}; - -export const getDefaultEmptyEntry = (): EmptyEntry => ({ - id: uuid.v4(), - field: '', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: '', -}); - -export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({ - id: uuid.v4(), - field: '', - type: OperatorTypeEnum.NESTED, - entries: [], -}); - -export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean => - items.some((item) => item.entries.some((entry) => entry.type === OperatorTypeEnum.LIST)); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx deleted file mode 100644 index 64801bd1892ed..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { storiesOf, addDecorator } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { BuilderLogicButtons } from './logic_buttons'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -storiesOf('Exceptions/BuilderLogicButtons', module) - .add('and/or buttons', () => { - return ( - - ); - }) - .add('nested button - isNested false', () => { - return ( - - ); - }) - .add('nested button - isNested true', () => { - return ( - - ); - }) - .add('and disabled', () => { - return ( - - ); - }) - .add('or disabled', () => { - return ( - - ); - }) - .add('nested disabled', () => { - return ( - - ); - }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts deleted file mode 100644 index dbac7d325b63a..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts +++ /dev/null @@ -1,521 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; -import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock'; -import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; - -import { ExceptionsBuilderExceptionItem } from '../types'; -import { Action, State, exceptionsBuilderReducer } from './reducer'; -import { getDefaultEmptyEntry } from './helpers'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); - -const initialState: State = { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: false, - addNested: false, - exceptions: [], - exceptionsToDelete: [], - errorExists: 0, -}; - -describe('exceptionsBuilderReducer', () => { - let reducer: (state: State, action: Action) => State; - - beforeEach(() => { - reducer = exceptionsBuilderReducer(); - }); - - describe('#setExceptions', () => { - test('should return "andLogicIncluded" ', () => { - const update = reducer(initialState, { - type: 'setExceptions', - exceptions: [], - }); - - expect(update).toEqual({ - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: false, - addNested: false, - exceptions: [], - exceptionsToDelete: [], - errorExists: 0, - }); - }); - - test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryMatchMock()], - }, - ]; - const { andLogicIncluded } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(andLogicIncluded).toBeTruthy(); - }); - - test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { andLogicIncluded } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(andLogicIncluded).toBeFalsy(); - }); - - test('should set "addNested" to true if last exception entry is type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - ]; - const { addNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(addNested).toBeTruthy(); - }); - - test('should set "addNested" to false if last exception item entry is not type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { addNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(addNested).toBeFalsy(); - }); - - test('should set "disableOr" to true if last exception entry is type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - ]; - const { disableOr } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableOr).toBeTruthy(); - }); - - test('should set "disableOr" to false if last exception item entry is not type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { disableOr } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableOr).toBeFalsy(); - }); - - test('should set "disableNested" to true if an exception item includes an entry of type list', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryListMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - ]; - const { disableNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableNested).toBeTruthy(); - }); - - test('should set "disableNested" to false if an exception item does not include an entry of type list', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { disableNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableNested).toBeFalsy(); - }); - - // What does that even mean?! :) Just checking if a user has selected - // to add a nested entry but has not yet selected the nested field - test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryListMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], - }, - ]; - const { disableAnd } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableAnd).toBeTruthy(); - }); - - test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { disableAnd } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableAnd).toBeFalsy(); - }); - }); - - describe('#setDefault', () => { - test('should restore initial state and add default empty entry to item" ', () => { - const update = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDefault', - initialState, - lastException: { - ...getExceptionListItemSchemaMock(), - entries: [], - }, - } - ); - - expect(update).toEqual({ - ...initialState, - exceptions: [ - { - ...getExceptionListItemSchemaMock(), - entries: [getDefaultEmptyEntry()], - }, - ], - }); - }); - }); - - describe('#setExceptionsToDelete', () => { - test('should add passed in exception item to "exceptionsToDelete"', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - id: '1', - entries: [getEntryListMock()], - }, - { - ...getExceptionListItemSchemaMock(), - id: '2', - entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], - }, - ]; - const { exceptionsToDelete } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions, - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setExceptionsToDelete', - exceptions: [ - { - ...getExceptionListItemSchemaMock(), - id: '1', - entries: [getEntryListMock()], - }, - ], - } - ); - - expect(exceptionsToDelete).toEqual([ - { - ...getExceptionListItemSchemaMock(), - id: '1', - entries: [getEntryListMock()], - }, - ]); - }); - }); - - describe('#setDisableAnd', () => { - test('should set "disableAnd" to false if "action.shouldDisable" is false', () => { - const { disableAnd } = reducer( - { - disableAnd: true, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableAnd', - shouldDisable: false, - } - ); - - expect(disableAnd).toBeFalsy(); - }); - - test('should set "disableAnd" to true if "action.shouldDisable" is true', () => { - const { disableAnd } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableAnd', - shouldDisable: true, - } - ); - - expect(disableAnd).toBeTruthy(); - }); - }); - - describe('#setDisableOr', () => { - test('should set "disableOr" to false if "action.shouldDisable" is false', () => { - const { disableOr } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: true, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableOr', - shouldDisable: false, - } - ); - - expect(disableOr).toBeFalsy(); - }); - - test('should set "disableOr" to true if "action.shouldDisable" is true', () => { - const { disableOr } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableOr', - shouldDisable: true, - } - ); - - expect(disableOr).toBeTruthy(); - }); - }); - - describe('#setAddNested', () => { - test('should set "addNested" to false if "action.addNested" is false', () => { - const { addNested } = reducer( - { - disableAnd: false, - disableNested: true, - disableOr: false, - andLogicIncluded: true, - addNested: true, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setAddNested', - addNested: false, - } - ); - - expect(addNested).toBeFalsy(); - }); - - test('should set "disableOr" to true if "action.addNested" is true', () => { - const { addNested } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setAddNested', - addNested: true, - } - ); - - expect(addNested).toBeTruthy(); - }); - }); - - describe('#setErrorsExist', () => { - test('should increase "errorExists" by one if payload is "true"', () => { - const { errorExists } = reducer( - { - disableAnd: false, - disableNested: true, - disableOr: false, - andLogicIncluded: true, - addNested: true, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setErrorsExist', - errorExists: true, - } - ); - - expect(errorExists).toEqual(1); - }); - - test('should decrease "errorExists" by one if payload is "false"', () => { - const { errorExists } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 1, - }, - { - type: 'setErrorsExist', - errorExists: false, - } - ); - - expect(errorExists).toEqual(0); - }); - - test('should not decrease "errorExists" if decreasing would dip into negative numbers', () => { - const { errorExists } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setErrorsExist', - errorExists: false, - } - ); - - expect(errorExists).toEqual(0); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts deleted file mode 100644 index c05847fb626d2..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts +++ /dev/null @@ -1,72 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const FIELD = i18n.translate('xpack.securitySolution.exceptions.builder.fieldDescription', { - defaultMessage: 'Field', -}); - -export const OPERATOR = i18n.translate( - 'xpack.securitySolution.exceptions.builder.operatorDescription', - { - defaultMessage: 'Operator', - } -); - -export const VALUE = i18n.translate('xpack.securitySolution.exceptions.builder.valueDescription', { - defaultMessage: 'Value', -}); - -export const EXCEPTION_FIELD_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription', - { - defaultMessage: 'Search', - } -); - -export const EXCEPTION_FIELD_NESTED_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription', - { - defaultMessage: 'Search nested field', - } -); - -export const EXCEPTION_OPERATOR_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription', - { - defaultMessage: 'Operator', - } -); - -export const EXCEPTION_FIELD_VALUE_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription', - { - defaultMessage: 'Search field value...', - } -); - -export const EXCEPTION_FIELD_LISTS_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription', - { - defaultMessage: 'Search for list...', - } -); - -export const ADD_NESTED_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.exceptions.builder.addNestedDescription', - { - defaultMessage: 'Add nested condition', - } -); - -export const ADD_NON_NESTED_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.exceptions.builder.addNonNestedDescription', - { - defaultMessage: 'Add non-nested condition', - } -); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index a30e6f769c47e..a97e71de77abd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -21,13 +21,13 @@ import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; -import * as builder from '../builder'; import { getRulesEqlSchemaMock, getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; +import { ExceptionBuilder } from '../../../../shared_imports'; const mockTheme = getMockTheme({ eui: { @@ -46,19 +46,28 @@ jest.mock('../use_add_exception'); jest.mock('../../../containers/source'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); -jest.mock('../builder'); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); +jest.mock('../../../../shared_imports', () => { + const originalModule = jest.requireActual('../../../../shared_imports'); + + return { + ...originalModule, + ExceptionBuilder: { + ExceptionBuilderComponent: () => ({} as JSX.Element), + }, + }; +}); describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { ExceptionBuilderComponent = jest - .spyOn(builder, 'ExceptionBuilderComponent') + .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') .mockReturnValue(<>); (useSignalIndex as jest.Mock).mockReturnValue({ 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 e33478ad99660..2c996c600261b 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 @@ -22,7 +22,11 @@ import { EuiCallOut, } from '@elastic/eui'; -import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils'; +import { + hasEqlSequenceQuery, + isEqlRule, + isThresholdRule, +} from '../../../../../common/detection_engine/utils'; import { useFetchIndex } from '../../../containers/source'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; @@ -30,12 +34,12 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema, ExceptionListType, -} from '../../../../../public/lists_plugin_deps'; + ExceptionBuilder, +} from '../../../../../public/shared_imports'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useKibana } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { ExceptionBuilderComponent } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -44,6 +48,7 @@ import { entryHasListType, entryHasNonEcsType, lowercaseHashValues, + filterIndexPatterns, } from '../helpers'; import { Loader } from '../../loader'; import { ErrorInfo, ErrorCallout } from '../error_callout'; @@ -312,13 +317,17 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} {i18n.EXCEPTION_BUILDER_INFO} - diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 3463f521655cb..c4d18ec24faad 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -30,6 +30,7 @@ import { getFileCodeSignature, getProcessCodeSignature, retrieveAlertOsTypes, + filterIndexPatterns, } from './helpers'; import { AlertData, EmptyEntry } from './types'; import { @@ -49,6 +50,7 @@ import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/ import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock'; +import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { ENTRIES, ENTRIES_WITH_IDS, @@ -60,12 +62,45 @@ import { EntriesArray, OsTypeArray, } from '../../../../../lists/common/schemas'; -import { IIndexPattern } from 'src/plugins/data/common'; +import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('123'), })); +const getMockIndexPattern = (): IIndexPattern => ({ + fields, + id: '1234', + title: 'logstash-*', +}); + +const mockEndpointFields = [ + { + name: 'file.path.caseless', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, +]; + +export const getEndpointField = (name: string) => + mockEndpointFields.find((field) => field.name === name) as IFieldType; + describe('Exception helpers', () => { beforeEach(() => { moment.tz.setDefault('UTC'); @@ -75,6 +110,25 @@ describe('Exception helpers', () => { moment.tz.setDefault('Browser'); }); + describe('#filterIndexPatterns', () => { + test('it returns index patterns without filtering if list type is "detection"', () => { + const mockIndexPatterns = getMockIndexPattern(); + const output = filterIndexPatterns(mockIndexPatterns, 'detection'); + + expect(output).toEqual(mockIndexPatterns); + }); + + test('it returns filtered index patterns if list type is "endpoint"', () => { + const mockIndexPatterns = { + ...getMockIndexPattern(), + fields: [...fields, ...mockEndpointFields], + }; + const output = filterIndexPatterns(mockIndexPatterns, 'endpoint'); + + expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] }); + }); + }); + describe('#getOperatorType', () => { test('returns operator type "match" if entry.type is "match"', () => { const payload = getEntryMatchMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 43c3b6c082f1a..69ec3120a064b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -41,6 +41,7 @@ import { OsTypeArray, EntriesArray, osType, + ExceptionListType, } from '../../../shared_imports'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { validate } from '../../../../common/validate'; @@ -48,6 +49,19 @@ import { Ecs } from '../../../../common/ecs'; import { CodeSignature } from '../../../../common/ecs/file'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { addIdToItem, removeIdFromItem } from '../../../../common'; +import exceptionableFields from './exceptionable_fields.json'; + +export const filterIndexPatterns = ( + patterns: IIndexPattern, + type: ExceptionListType +): IIndexPattern => { + return type === 'endpoint' + ? { + ...patterns, + fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)), + } + : patterns; +}; export const addIdToEntries = (entries: EntriesArray): EntriesArray => { return entries.map((singleEntry) => { diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx new file mode 100644 index 0000000000000..a1547940765c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { repeat } from 'lodash/fp'; +import React from 'react'; + +import { LineClamp } from '.'; + +describe('LineClamp', () => { + const message = repeat(1000, 'abcdefghij '); // 10 characters, with a trailing space + + describe('no overflow', () => { + test('it does NOT render the expanded line clamp when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').exists()).toBe(false); + }); + + test('it does NOT render the styled line clamp expanded when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the default line clamp when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').first().text()).toBe(message); + }); + + test('it does NOT render the `Read More` button when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').exists()).toBe(false); + }); + }); + + describe('overflow', () => { + const clientHeight = 400; + const scrollHeight = clientHeight + 100; // scrollHeight is > clientHeight + + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, + value: clientHeight, + }); + + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { + configurable: true, + value: scrollHeight, + }); + }); + + test('it does NOT render the expanded line clamp by default when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the styled line clamp when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').first().text()).toBe(message); + }); + + test('it does NOT render the default line clamp when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the `Read More` button with the expected (default) text when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read More' + ); + }); + + describe('clicking the Read More button', () => { + test('it displays the `Read Less` button text after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read Less' + ); + }); + + test('it renders the expanded content after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first().text()).toBe(message); + }); + }); + + test('it renders the expanded content with a max-height of one third the view height when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first()).toHaveStyleRule( + 'max-height', + '33vh' + ); + }); + + test('it automatically vertically scrolls the content when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first()).toHaveStyleRule( + 'overflow-y', + 'auto' + ); + }); + + test('it does NOT render the styled line clamp after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').exists()).toBe(false); + }); + + test('it does NOT render the default line clamp after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').exists()).toBe(false); + }); + + test('it once again displays the `Read More` button text after the user clicks the `Read Less` when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); // 1st toggle + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); // 2nd toggle + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read More' // after the 2nd toggle, the button once-again says `Read More` + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx index d3bc4c2d50f98..896b0ec5fd8df 100644 --- a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import * as i18n from './translations'; const LINE_CLAMP = 3; -const LINE_CLAMP_HEIGHT = 4.5; +const LINE_CLAMP_HEIGHT = 5.5; const StyledLineClamp = styled.div` display: -webkit-box; @@ -28,6 +28,13 @@ const ReadMore = styled(EuiButtonEmpty)` } `; +const ExpandedContent = styled.div` + max-height: 33vh; + overflow-wrap: break-word; + overflow-x: hidden; + overflow-y: auto; +`; + const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) => { const [isOverflow, setIsOverflow] = useState(null); const [isExpanded, setIsExpanded] = useState(null); @@ -60,11 +67,15 @@ const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) return ( <> {isExpanded ? ( -

    {content}

    + +

    {content}

    +
    ) : isOverflow == null || isOverflow === true ? ( - {content} + + {content} + ) : ( - {content} + {content} )} {isOverflow && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.test.ts index 2a792c29fd37a..a89d34e2fa43d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HostItem } from '../../../../graphql/types'; +import { HostItem } from '../../../../../common/search_strategy/security_solution/hosts'; import { CriteriaFields } from '../types'; import { hostToCriteria } from './host_to_criteria'; @@ -28,6 +28,7 @@ describe('host_to_criteria', () => { test('returns an empty array if the host.name is null', () => { const hostItem: HostItem = { host: { + // @ts-expect-error name: null, }, }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts index 57a05dc943fa3..19eae99757849 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/criteria/host_to_criteria.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { HostItem } from '../../../../../common/search_strategy/security_solution/hosts'; import { CriteriaFields } from '../types'; -import { HostItem } from '../../../../graphql/types'; export const hostToCriteria = (hostItem: HostItem): CriteriaFields[] => { if (hostItem.host != null && hostItem.host.name != null) { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.ts index 2f64c6e043d6b..3a63958e2de12 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { HostItem } from '../../../../../common/search_strategy/security_solution/hosts'; import { InfluencerInput } from '../types'; -import { HostItem } from '../../../../graphql/types'; export const hostToInfluencers = (hostItem: HostItem): InfluencerInput[] | null => { if (hostItem.host != null && hostItem.host.name != null) { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 6c88b8e29800b..cf6db52d0cece 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -48,6 +48,8 @@ import { import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { buildTimeRangeFilter } from './helpers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -336,6 +338,8 @@ export const AlertsTableComponent: React.FC = ({ headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.detections} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index b2e5638ff120e..26b9662a8f19b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -215,19 +215,20 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const openAlertActionComponent = ( - - {i18n.ACTION_OPEN_ALERT} - - ); + const openAlertActionComponent = useMemo(() => { + return ( + + {i18n.ACTION_OPEN_ALERT} + + ); + }, [openAlertActionOnClick, hasIndexUpdateDelete, hasIndexMaintenance]); const closeAlertActionClick = useCallback(() => { updateAlertStatusAction({ @@ -248,19 +249,20 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const closeAlertActionComponent = ( - - {i18n.ACTION_CLOSE_ALERT} - - ); + const closeAlertActionComponent = useMemo(() => { + return ( + + {i18n.ACTION_CLOSE_ALERT} + + ); + }, [closeAlertActionClick, hasIndexUpdateDelete, hasIndexMaintenance]); const inProgressAlertActionClick = useCallback(() => { updateAlertStatusAction({ @@ -281,72 +283,77 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const inProgressAlertActionComponent = ( - - {i18n.ACTION_IN_PROGRESS_ALERT} - - ); - - const button = ( - - - - ); + const inProgressAlertActionComponent = useMemo(() => { + return ( + + {i18n.ACTION_IN_PROGRESS_ALERT} + + ); + }, [canUserCRUD, hasIndexUpdateDelete, inProgressAlertActionClick]); + + const button = useMemo(() => { + return ( + + + + ); + }, [disabled, onButtonClick, ariaLabel]); const handleAddEndpointExceptionClick = useCallback((): void => { closePopover(); setOpenAddExceptionModal('endpoint'); }, [closePopover]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const addEndpointExceptionComponent = ( - - {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} - - ); + const addEndpointExceptionComponent = useMemo(() => { + return ( + + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + + ); + }, [canUserCRUD, hasIndexWrite, isEndpointAlert, handleAddEndpointExceptionClick]); const handleAddExceptionClick = useCallback((): void => { closePopover(); setOpenAddExceptionModal('detection'); }, [closePopover]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const addExceptionComponent = ( - - - {i18n.ACTION_ADD_EXCEPTION} - - - ); + const addExceptionComponent = useMemo(() => { + return ( + + + {i18n.ACTION_ADD_EXCEPTION} + + + ); + }, [handleAddExceptionClick, canUserCRUD, hasIndexWrite]); const statusFilters = useMemo(() => { if (!alertStatus) { diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 1df8716ba76e4..0a41ca05b8753 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -687,231 +687,6 @@ }, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "Hosts", - "description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified", - "args": [ - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "pagination", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PaginationInputPaginated", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sort", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "HostsSortField", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "docValueFields", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "docValueFieldsInput", - "ofType": null - } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostsData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "HostOverview", - "description": "", - "args": [ - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "hostName", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostItem", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "HostFirstLastSeen", - "description": "", - "args": [ - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "hostName", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "docValueFields", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "docValueFieldsInput", - "ofType": null - } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "FirstLastSeenHost", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null } ], "inputFields": null, @@ -1104,851 +879,7 @@ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Boolean", - "description": "The `Boolean` scalar type represents `true` or `false`.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "TimerangeInput", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "interval", - "description": "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "to", - "description": "The end of the timerange", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "from", - "description": "The beginning of the timerange", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "PaginationInputPaginated", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "activePage", - "description": "The activePage parameter defines the page of results you want to fetch", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "cursorStart", - "description": "The cursorStart parameter defines the start of the results to be displayed", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "fakePossibleCount", - "description": "The fakePossibleCount parameter determines the total count in order to show 5 additional pages", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "querySize", - "description": "The querySize parameter is the number of items to be returned", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "HostsSortField", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "field", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "HostsFields", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "direction", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "HostsFields", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "hostName", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "docValueFieldsInput", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "field", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "format", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostsData", - "description": "", - "fields": [ - { - "name": "edges", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostsEdges", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostsEdges", - "description": "", - "fields": [ - { - "name": "node", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostItem", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostItem", - "description": "", - "fields": [ - { - "name": "_id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "agent", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "AgentFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cloud", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endpoint", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "EndpointFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "host", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AgentFields", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CloudFields", - "description": "", - "fields": [ - { - "name": "instance", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "CloudInstance", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "machine", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "CloudMachine", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "provider", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "region", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CloudInstance", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CloudMachine", - "description": "", - "fields": [ - { - "name": "type", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "EndpointFields", - "description": "", - "fields": [ - { - "name": "endpointPolicy", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sensorVersion", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "policyStatus", - "description": "", - "args": [], - "type": { "kind": "ENUM", "name": "HostPolicyResponseActionStatus", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "HostPolicyResponseActionStatus", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "success", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "failure", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "warning", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unsupported", - "description": "", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostEcsFields", - "description": "", - "fields": [ - { - "name": "architecture", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ip", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mac", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "os", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "OsEcsFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "ToStringArray", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OsEcsFields", - "description": "", - "fields": [ - { - "name": "platform", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "full", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "family", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "version", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "kernel", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Inspect", - "description": "", - "fields": [ - { - "name": "dsl", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "response", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Date", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CursorType", - "description": "", - "fields": [ - { - "name": "value", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tiebreaker", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PageInfoPaginated", - "description": "", - "fields": [ - { - "name": "activePage", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "fakeTotalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "showMorePagesIndicator", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "FirstLastSeenHost", - "description": "", - "fields": [ - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "firstSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + }, "isDeprecated": false, "deprecationReason": null } @@ -1958,6 +889,16 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "TimelineType", @@ -5045,6 +3986,16 @@ ], "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "ToStringArray", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "ToStringArrayNoNullable", @@ -5523,6 +4474,132 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "OsEcsFields", + "description": "", + "fields": [ + { + "name": "platform", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "full", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "family", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kernel", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "HostEcsFields", + "description": "", + "fields": [ + { + "name": "architecture", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ip", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mac", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "os", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "OsEcsFields", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Thread", @@ -7900,146 +6977,57 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "EcsEdges", - "description": "", - "fields": [ - { - "name": "node", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "ECS", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OsFields", + "kind": "SCALAR", + "name": "Date", "description": "", - "fields": [ - { - "name": "platform", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "full", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "family", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "version", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "kernel", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], + "fields": null, "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", - "name": "HostFields", + "name": "EcsEdges", "description": "", "fields": [ { - "name": "architecture", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ip", + "name": "node", "description": "", "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "ECS", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "mac", + "name": "cursor", "description": "", "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CursorType", + "description": "", + "fields": [ { - "name": "name", + "name": "value", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -8047,15 +7035,7 @@ "deprecationReason": null }, { - "name": "os", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "OsFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", + "name": "tiebreaker", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -8205,6 +7185,78 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "TimerangeInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "interval", + "description": "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "to", + "description": "The end of the timerange", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "from", + "description": "The beginning of the timerange", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "field", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "format", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PaginationInput", @@ -8238,6 +7290,57 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "PaginationInputPaginated", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "activePage", + "description": "The activePage parameter defines the page of results you want to fetch", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "cursorStart", + "description": "The cursorStart parameter defines the start of the results to be displayed", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "fakePossibleCount", + "description": "The fakePossibleCount parameter determines the total count in order to show 5 additional pages", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "querySize", + "description": "The querySize parameter is the number of items to be returned", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "FlowTarget", @@ -8357,6 +7460,104 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Inspect", + "description": "", + "fields": [ + { + "name": "dsl", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "response", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfoPaginated", + "description": "", + "fields": [ + { + "name": "activePage", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fakeTotalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showMorePagesIndicator", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "FavoriteTimelineInput", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 88c1ec4e6e328..8ffd2995d0d97 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -21,38 +21,6 @@ export interface SortNote { sortOrder: Direction; } -export interface TimerangeInput { - /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ - interval: string; - /** The end of the timerange */ - to: string; - /** The beginning of the timerange */ - from: string; -} - -export interface PaginationInputPaginated { - /** The activePage parameter defines the page of results you want to fetch */ - activePage: number; - /** The cursorStart parameter defines the start of the results to be displayed */ - cursorStart: number; - /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ - fakePossibleCount: number; - /** The querySize parameter is the number of items to be returned */ - querySize: number; -} - -export interface HostsSortField { - field: HostsFields; - - direction: Direction; -} - -export interface DocValueFieldsInput { - field: string; - - format: string; -} - export interface PageInfoTimeline { pageIndex: number; @@ -245,6 +213,21 @@ export interface SortTimelineInput { sortDirection?: Maybe; } +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface DocValueFieldsInput { + field: string; + + format: string; +} + export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -254,6 +237,17 @@ export interface PaginationInput { tiebreaker?: Maybe; } +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + export interface SortField { sortFieldId: string; @@ -278,18 +272,6 @@ export enum Direction { desc = 'desc', } -export enum HostsFields { - hostName = 'hostName', - lastSeen = 'lastSeen', -} - -export enum HostPolicyResponseActionStatus { - success = 'success', - failure = 'failure', - warning = 'warning', - unsupported = 'unsupported', -} - export enum TimelineType { default = 'default', template = 'template', @@ -349,12 +331,10 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export type ToStringArray = string[]; - -export type Date = string; - export type ToAny = any; +export type ToStringArray = string[]; + export type ToStringArrayNoNullable = any; export type ToDateArray = string[]; @@ -363,6 +343,8 @@ export type ToNumberArray = number[]; export type ToBooleanArray = boolean[]; +export type Date = string; + export type ToIFieldSubTypeNonNullable = any; // ==================================================== @@ -452,12 +434,6 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ - Hosts: HostsData; - - HostOverview: HostItem; - - HostFirstLastSeen: FirstLastSeenHost; } /** A set of configuration options for a security data source */ @@ -490,126 +466,6 @@ export interface SourceStatus { indexFields: string[]; } -export interface HostsData { - edges: HostsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface HostsEdges { - node: HostItem; - - cursor: CursorType; -} - -export interface HostItem { - _id?: Maybe; - - agent?: Maybe; - - cloud?: Maybe; - - endpoint?: Maybe; - - host?: Maybe; - - inspect?: Maybe; - - lastSeen?: Maybe; -} - -export interface AgentFields { - id?: Maybe; -} - -export interface CloudFields { - instance?: Maybe; - - machine?: Maybe; - - provider?: Maybe<(Maybe)[]>; - - region?: Maybe<(Maybe)[]>; -} - -export interface CloudInstance { - id?: Maybe<(Maybe)[]>; -} - -export interface CloudMachine { - type?: Maybe<(Maybe)[]>; -} - -export interface EndpointFields { - endpointPolicy?: Maybe; - - sensorVersion?: Maybe; - - policyStatus?: Maybe; -} - -export interface HostEcsFields { - architecture?: Maybe; - - id?: Maybe; - - ip?: Maybe; - - mac?: Maybe; - - name?: Maybe; - - os?: Maybe; - - type?: Maybe; -} - -export interface OsEcsFields { - platform?: Maybe; - - name?: Maybe; - - full?: Maybe; - - family?: Maybe; - - version?: Maybe; - - kernel?: Maybe; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface CursorType { - value?: Maybe; - - tiebreaker?: Maybe; -} - -export interface PageInfoPaginated { - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; -} - -export interface FirstLastSeenHost { - inspect?: Maybe; - - firstSeen?: Maybe; - - lastSeen?: Maybe; -} - export interface TimelineResult { columns?: Maybe; @@ -985,6 +841,36 @@ export interface AuditdEcsFields { sequence?: Maybe; } +export interface OsEcsFields { + platform?: Maybe; + + name?: Maybe; + + full?: Maybe; + + family?: Maybe; + + version?: Maybe; + + kernel?: Maybe; +} + +export interface HostEcsFields { + architecture?: Maybe; + + id?: Maybe; + + ip?: Maybe; + + mac?: Maybe; + + name?: Maybe; + + os?: Maybe; + + type?: Maybe; +} + export interface Thread { id?: Maybe; @@ -1547,34 +1433,10 @@ export interface EcsEdges { cursor: CursorType; } -export interface OsFields { - platform?: Maybe; - - name?: Maybe; - - full?: Maybe; - - family?: Maybe; - - version?: Maybe; - - kernel?: Maybe; -} - -export interface HostFields { - architecture?: Maybe; - - id?: Maybe; - - ip?: Maybe<(Maybe)[]>; - - mac?: Maybe<(Maybe)[]>; - - name?: Maybe; - - os?: Maybe; +export interface CursorType { + value?: Maybe; - type?: Maybe; + tiebreaker?: Maybe; } /** A descriptor of a field in an index */ @@ -1609,6 +1471,20 @@ export interface PageInfo { hasNextPage?: Maybe; } +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface PageInfoPaginated { + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; +} + // ==================================================== // Arguments // ==================================================== @@ -1654,39 +1530,6 @@ export interface GetAllTimelineQueryArgs { status?: Maybe; } -export interface HostsSourceArgs { - id?: Maybe; - - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - sort: HostsSortField; - - filterQuery?: Maybe; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; -} -export interface HostOverviewSourceArgs { - id?: Maybe; - - hostName: string; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface HostFirstLastSeenSourceArgs { - id?: Maybe; - - hostName: string; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -1745,254 +1588,6 @@ export interface DeleteTimelineMutationArgs { // Documents // ==================================================== -export namespace GetHostOverviewQuery { - export type Variables = { - sourceId: string; - hostName: string; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostOverview: HostOverview; - }; - - export type HostOverview = { - __typename?: 'HostItem'; - - _id: Maybe; - - agent: Maybe; - - host: Maybe; - - cloud: Maybe; - - inspect: Maybe; - - endpoint: Maybe; - }; - - export type Agent = { - __typename?: 'AgentFields'; - - id: Maybe; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe; - - id: Maybe; - - ip: Maybe; - - mac: Maybe; - - name: Maybe; - - os: Maybe; - - type: Maybe; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe; - - name: Maybe; - - platform: Maybe; - - version: Maybe; - }; - - export type Cloud = { - __typename?: 'CloudFields'; - - instance: Maybe; - - machine: Maybe; - - provider: Maybe<(Maybe)[]>; - - region: Maybe<(Maybe)[]>; - }; - - export type Instance = { - __typename?: 'CloudInstance'; - - id: Maybe<(Maybe)[]>; - }; - - export type Machine = { - __typename?: 'CloudMachine'; - - type: Maybe<(Maybe)[]>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type Endpoint = { - __typename?: 'EndpointFields'; - - endpointPolicy: Maybe; - - policyStatus: Maybe; - - sensorVersion: Maybe; - }; -} - -export namespace GetHostFirstLastSeenQuery { - export type Variables = { - sourceId: string; - hostName: string; - defaultIndex: string[]; - docValueFields: DocValueFieldsInput[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostFirstLastSeen: HostFirstLastSeen; - }; - - export type HostFirstLastSeen = { - __typename?: 'FirstLastSeenHost'; - - firstSeen: Maybe; - - lastSeen: Maybe; - }; -} - -export namespace GetHostsTableQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - sort: HostsSortField; - filterQuery?: Maybe; - defaultIndex: string[]; - inspect: boolean; - docValueFields: DocValueFieldsInput[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Hosts: Hosts; - }; - - export type Hosts = { - __typename?: 'HostsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe; - }; - - export type Edges = { - __typename?: 'HostsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'HostItem'; - - _id: Maybe; - - lastSeen: Maybe; - - host: Maybe; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe; - - name: Maybe; - - os: Maybe; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - name: Maybe; - - version: Maybe; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - export namespace GetAllTimeline { export type Variables = { pageInfo: PageInfoTimeline; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap index 1d70f4f72ac8b..59a00cbf190f6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap @@ -6,73 +6,38 @@ exports[`Hosts Table rendering it renders the default Hosts table 1`] = ` Array [ Object { "cursor": Object { - "value": "98966fa2013c396155c460d35c0902be", + "tiebreaker": null, + "value": "beats-ci-immutable-ubuntu-1804-1615475026535098510", }, "node": Object { - "_id": "cPsuhGcB0WOhS6qyTKC0", + "_id": "beats-ci-immutable-ubuntu-1804-1615475026535098510", "host": Object { "name": Array [ - "elrond.elstc.co", + "beats-ci-immutable-ubuntu-1804-1615475026535098510", ], "os": Object { "name": Array [ "Ubuntu", ], "version": Array [ - "18.04.1 LTS (Bionic Beaver)", - ], - }, - }, - }, - }, - Object { - "cursor": Object { - "value": "aa7ca589f1b8220002f2fc61c64cfbf1", - }, - "node": Object { - "_id": "KwQDiWcB0WOhS6qyXmrW", - "cloud": Object { - "instance": Object { - "id": Array [ - "423232333829362673777", - ], - }, - "machine": Object { - "type": Array [ - "custom-4-16384", - ], - }, - "provider": Array [ - "gce", - ], - "region": Array [ - "us-east-1", - ], - }, - "host": Object { - "name": Array [ - "siem-kibana", - ], - "os": Object { - "name": Array [ - "Debian GNU/Linux", - ], - "version": Array [ - "9 (stretch)", + "18.04.5 LTS (Bionic Beaver)", ], }, }, + "lastSeen": Array [ + "2021-03-11T15:05:36.783Z", + ], }, }, ] } - fakeTotalCount={50} + fakeTotalCount={0} id="hostsQuery" isInspect={false} loadPage={[MockFunction]} loading={false} - showMorePagesIndicator={true} - totalCount={4} + showMorePagesIndicator={false} + totalCount={-1} type="page" /> `; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx index 6f43a18431a27..d6c51b2bfe05e 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx @@ -78,7 +78,7 @@ export const getHostsColumns = (): HostsTableColumns => [ hideForMobile: false, sortable: true, render: (lastSeen: Maybe | undefined) => { - if (lastSeen != null) { + if (lastSeen != null && lastSeen.length > 0) { return ( { const wrapper = shallow( @@ -93,14 +92,10 @@ describe('Hosts Table', () => { id="hostsQuery" isInspect={false} loading={false} - data={mockData.Hosts.edges} - totalCount={mockData.Hosts.totalCount} - fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.Hosts.pageInfo)} - showMorePagesIndicator={getOr( - false, - 'showMorePagesIndicator', - mockData.Hosts.pageInfo - )} + data={mockData} + totalCount={0} + fakeTotalCount={-1} + showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} /> diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index 75bf524ab1e86..d20333d210559 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -9,15 +9,6 @@ import React, { useMemo, useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { assertUnreachable } from '../../../../common/utility_types'; -import { - Direction, - HostFields, - HostItem, - HostsEdges, - HostsFields, - HostsSortField, - OsFields, -} from '../../../graphql/types'; import { Columns, Criteria, @@ -29,6 +20,14 @@ import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import { getHostsColumns } from './columns'; import * as i18n from './translations'; +import { + HostsEdges, + HostItem, + HostsSortField, + HostsFields, +} from '../../../../common/search_strategy/security_solution/hosts'; +import { Direction } from '../../../../common/search_strategy'; +import { HostEcs, OsEcs } from '../../../../common/ecs/host'; const tableType = hostsModel.HostsTableType.hosts; @@ -45,10 +44,10 @@ interface HostsTableProps { } export type HostsTableColumns = [ - Columns, + Columns, Columns, - Columns, - Columns + Columns, + Columns ]; const rowItems: ItemsPerRow[] = [ @@ -82,7 +81,6 @@ const HostsTableComponent: React.FC = ({ const { activePage, direction, limit, sortField } = useDeepEqualSelector((state) => getHostsSelector(state, type) ); - const updateLimitPagination = useCallback( (newLimit) => dispatch( @@ -178,6 +176,7 @@ const getNodeField = (field: HostsFields): string => { } assertUnreachable(field); }; + export const HostsTable = React.memo(HostsTableComponent); HostsTable.displayName = 'HostsTable'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/mock.ts index 9c5d7bb152d91..525d5ce368147 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/mock.ts @@ -5,57 +5,18 @@ * 2.0. */ -import { HostsData } from '../../../graphql/types'; +import { HostsEdges } from '../../../../common/search_strategy/security_solution/hosts'; -export const mockData: { Hosts: HostsData } = { - Hosts: { - totalCount: 4, - edges: [ - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - host: { - name: ['elrond.elstc.co'], - os: { - name: ['Ubuntu'], - version: ['18.04.1 LTS (Bionic Beaver)'], - }, - }, - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, +export const mockData: HostsEdges[] = [ + { + node: { + _id: 'beats-ci-immutable-ubuntu-1804-1615475026535098510', + lastSeen: ['2021-03-11T15:05:36.783Z'], + host: { + name: ['beats-ci-immutable-ubuntu-1804-1615475026535098510'], + os: { name: ['Ubuntu'], version: ['18.04.5 LTS (Bionic Beaver)'] }, }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - host: { - name: ['siem-kibana'], - os: { - name: ['Debian GNU/Linux'], - version: ['9 (stretch)'], - }, - }, - cloud: { - instance: { - id: ['423232333829362673777'], - }, - machine: { - type: ['custom-4-16384'], - }, - provider: ['gce'], - region: ['us-east-1'], - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, }, + cursor: { value: 'beats-ci-immutable-ubuntu-1804-1615475026535098510', tiebreaker: null }, }, -}; +]; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx deleted file mode 100644 index 532b9f262e136..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx +++ /dev/null @@ -1,161 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// REPLACE WHEN HOST ENDPOINT DATA IS AVAILABLE - -import deepEqual from 'fast-deep-equal'; -import { noop } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { Subscription } from 'rxjs'; - -import { inputsModel } from '../../../../common/store'; -import { useKibana } from '../../../../common/lib/kibana'; -import { - HostItem, - HostsQueries, - HostDetailsRequestOptions, - HostDetailsStrategyResponse, -} from '../../../../../common/search_strategy/security_solution/hosts'; - -import * as i18n from './translations'; -import { - isCompleteResponse, - isErrorResponse, -} from '../../../../../../../../src/plugins/data/common'; -import { getInspectResponse } from '../../../../helpers'; -import { InspectResponse } from '../../../../types'; - -const ID = 'hostsDetailsQuery'; - -export interface HostDetailsArgs { - id: string; - inspect: InspectResponse; - hostDetails: HostItem; - refetch: inputsModel.Refetch; - startDate: string; - endDate: string; -} - -interface UseHostDetails { - endDate: string; - hostName: string; - id?: string; - indexNames: string[]; - skip?: boolean; - startDate: string; -} - -export const useHostDetails = ({ - endDate, - hostName, - indexNames, - id = ID, - skip = false, - startDate, -}: UseHostDetails): [boolean, HostDetailsArgs] => { - const { data, notifications } = useKibana().services; - const refetch = useRef(noop); - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(new Subscription()); - const [loading, setLoading] = useState(false); - const [hostDetailsRequest, setHostDetailsRequest] = useState( - null - ); - - const [hostDetailsResponse, setHostDetailsResponse] = useState({ - endDate, - hostDetails: {}, - id, - inspect: { - dsl: [], - response: [], - }, - refetch: refetch.current, - startDate, - }); - - const hostDetailsSearch = useCallback( - (request: HostDetailsRequestOptions | null) => { - if (request == null || skip) { - return; - } - - const asyncSearch = async () => { - abortCtrl.current = new AbortController(); - setLoading(true); - - searchSubscription$.current = data.search - .search(request, { - strategy: 'securitySolutionSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - setLoading(false); - setHostDetailsResponse((prevResponse) => ({ - ...prevResponse, - hostDetails: response.hostDetails, - inspect: getInspectResponse(response, prevResponse.inspect), - refetch: refetch.current, - })); - searchSubscription$.current.unsubscribe(); - } else if (isErrorResponse(response)) { - setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_HOST_OVERVIEW); - searchSubscription$.current.unsubscribe(); - } - }, - error: (msg) => { - setLoading(false); - notifications.toasts.addDanger({ - title: i18n.FAIL_HOST_OVERVIEW, - text: msg.message, - }); - searchSubscription$.current.unsubscribe(); - }, - }); - }; - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - asyncSearch(); - refetch.current = asyncSearch; - }, - [data.search, notifications.toasts, skip] - ); - - useEffect(() => { - setHostDetailsRequest((prevRequest) => { - const myRequest = { - ...(prevRequest ?? {}), - defaultIndex: indexNames, - factoryQueryType: HostsQueries.details, - hostName, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - }; - if (!deepEqual(prevRequest, myRequest)) { - return myRequest; - } - return prevRequest; - }); - return () => { - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - }; - }, [endDate, hostName, indexNames, startDate]); - - useEffect(() => { - hostDetailsSearch(hostDetailsRequest); - }, [hostDetailsRequest, hostDetailsSearch]); - - return [loading, hostDetailsResponse]; -}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts deleted file mode 100644 index 70c07bc55fd23..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import gql from 'graphql-tag'; - -export const HostOverviewQuery = gql` - query GetHostOverviewQuery( - $sourceId: ID! - $hostName: String! - $timerange: TimerangeInput! - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - HostOverview(hostName: $hostName, timerange: $timerange, defaultIndex: $defaultIndex) { - _id - agent { - id - } - host { - architecture - id - ip - mac - name - os { - family - name - platform - version - } - type - } - cloud { - instance { - id - } - machine { - type - } - provider - region - } - inspect @include(if: $inspect) { - dsl - response - } - endpoint { - endpointPolicy - policyStatus - sensorVersion - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx index 26b4c0e55e56a..1eaa89575de26 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx @@ -5,107 +5,155 @@ * 2.0. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; -import { inputsModel, inputsSelectors, State } from '../../../../common/store'; -import { getDefaultFetchPolicy } from '../../../../common/containers/helpers'; -import { QueryTemplate, QueryTemplateProps } from '../../../../common/containers/query_template'; +import { inputsModel } from '../../../../common/store'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostItem, + HostsQueries, + HostDetailsRequestOptions, + HostDetailsStrategyResponse, +} from '../../../../../common/search_strategy/security_solution/hosts'; -import { HostOverviewQuery } from './host_overview.gql_query'; -import { GetHostOverviewQuery, HostItem } from '../../../../graphql/types'; +import * as i18n from './translations'; +import { + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../../helpers'; +import { InspectResponse } from '../../../../types'; -const ID = 'hostOverviewQuery'; +export const ID = 'hostsDetailsQuery'; -export interface HostOverviewArgs { +export interface HostDetailsArgs { id: string; - inspect: inputsModel.InspectQuery; - hostOverview: HostItem; - loading: boolean; + inspect: InspectResponse; + hostDetails: HostItem; refetch: inputsModel.Refetch; startDate: string; endDate: string; } -export interface HostOverviewReduxProps { - isInspected: boolean; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: HostOverviewArgs) => React.ReactNode; +interface UseHostDetails { + endDate: string; hostName: string; + id?: string; + indexNames: string[]; + skip?: boolean; startDate: string; - endDate: string; } -type HostsOverViewProps = OwnProps & HostOverviewReduxProps; +export const useHostDetails = ({ + endDate, + hostName, + indexNames, + id = ID, + skip = false, + startDate, +}: UseHostDetails): [boolean, HostDetailsArgs] => { + const { data, notifications } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [hostDetailsRequest, setHostDetailsRequest] = useState( + null + ); + + const [hostDetailsResponse, setHostDetailsResponse] = useState({ + endDate, + hostDetails: {}, + id, + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + startDate, + }); + + const hostDetailsSearch = useCallback( + (request: HostDetailsRequestOptions | null) => { + if (request == null || skip) { + return; + } -class HostOverviewByNameComponentQuery extends QueryTemplate< - HostsOverViewProps, - GetHostOverviewQuery.Query, - GetHostOverviewQuery.Variables -> { - public render() { - const { - id = ID, - indexNames, - isInspected, - children, - hostName, - skip, - sourceId, - startDate, - endDate, - } = this.props; - return ( - - query={HostOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - hostName, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - defaultIndex: indexNames, - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const hostOverview = getOr([], 'source.HostOverview', data); - return children({ - id, - inspect: getOr(null, 'source.HostOverview.inspect', data), - refetch, - loading, - hostOverview, - startDate, - endDate, + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription$.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setLoading(false); + setHostDetailsResponse((prevResponse) => ({ + ...prevResponse, + hostDetails: response.hostDetails, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + searchSubscription$.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_HOST_OVERVIEW); + searchSubscription$.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + notifications.toasts.addDanger({ + title: i18n.FAIL_HOST_OVERVIEW, + text: msg.message, + }); + searchSubscription$.current.unsubscribe(); + }, }); - }} - - ); - } -} + }; + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, notifications.toasts, skip] + ); -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, + useEffect(() => { + setHostDetailsRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + defaultIndex: indexNames, + factoryQueryType: HostsQueries.details, + hostName, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + return () => { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); }; - }; - return mapStateToProps; -}; + }, [endDate, hostName, indexNames, startDate]); -export const HostOverviewByNameQuery = compose>( - connect(makeMapStateToProps) -)(HostOverviewByNameComponentQuery); + useEffect(() => { + hostDetailsSearch(hostDetailsRequest); + }, [hostDetailsRequest, hostDetailsSearch]); + + return [loading, hostDetailsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts deleted file mode 100644 index 789a1d4ff5a82..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import gql from 'graphql-tag'; - -export const HostFirstLastSeenGqlQuery = gql` - query GetHostFirstLastSeenQuery( - $sourceId: ID! - $hostName: String! - $defaultIndex: [String!]! - $docValueFields: [docValueFieldsInput!]! - ) { - source(id: $sourceId) { - id - HostFirstLastSeen( - hostName: $hostName - defaultIndex: $defaultIndex - docValueFields: $docValueFields - ) { - firstSeen - lastSeen - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index bd49d6be34e5c..380e6b05471a8 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -17,7 +17,7 @@ import { } from '../../../../../common/search_strategy/security_solution'; import * as i18n from './translations'; -import { DocValueFields } from '../../../../../common/search_strategy'; +import { Direction, DocValueFields } from '../../../../../common/search_strategy'; import { isCompleteResponse, isErrorResponse, @@ -30,13 +30,13 @@ export interface FirstLastSeenHostArgs { errorMessage: string | null; firstSeen?: string | null; lastSeen?: string | null; - order: 'asc' | 'desc' | null; + order: Direction.asc | Direction.desc | null; } interface UseHostFirstLastSeen { docValueFields: DocValueFields[]; hostName: string; indexNames: string[]; - order: 'asc' | 'desc'; + order: Direction.asc | Direction.desc; } export const useFirstLastSeenHost = ({ diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts deleted file mode 100644 index 35fba31eeaa58..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import gql from 'graphql-tag'; - -export const HostsTableQuery = gql` - query GetHostsTableQuery( - $sourceId: ID! - $timerange: TimerangeInput! - $pagination: PaginationInputPaginated! - $sort: HostsSortField! - $filterQuery: String - $defaultIndex: [String!]! - $inspect: Boolean! - $docValueFields: [docValueFieldsInput!]! - ) { - source(id: $sourceId) { - id - Hosts( - timerange: $timerange - pagination: $pagination - sort: $sort - filterQuery: $filterQuery - defaultIndex: $defaultIndex - docValueFields: $docValueFields - ) { - totalCount - edges { - node { - _id - lastSeen - host { - id - name - os { - name - version - } - } - } - cursor { - value - } - } - pageInfo { - activePage - fakeTotalCount - showMorePagesIndicator - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index faa240f98e53e..1ff4abb78b210 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -24,10 +24,8 @@ import { scoreIntervalToDateTime } from '../../../common/components/ml/score/sco import { SiemNavigation } from '../../../common/components/navigation'; import { HostsDetailsKpiComponent } from '../../components/kpi_hosts'; import { HostOverview } from '../../../overview/components/host_overview'; -import { manageQuery } from '../../../common/components/page/manage_query'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; -import { HostOverviewByNameQuery } from '../../containers/hosts/details'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useKibana } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -51,8 +49,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; - -const HostOverviewManage = manageQuery(HostOverview); +import { useHostDetails } from '../../containers/hosts/details'; const HostDetailsComponent: React.FC = ({ detailName, hostDetailsPagePath }) => { const dispatch = useDispatch(); @@ -96,6 +93,12 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta ); const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); + const [loading, { hostDetails: hostOverview, id }] = useHostDetails({ + endDate: to, + startDate: from, + hostName: detailName, + indexNames: selectedPatterns, + }); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -131,49 +134,35 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta title={detailName} /> - - {({ hostOverview, loading, id, inspect, refetch }) => ( - ( + - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - + narrowDateRange={(score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> )} - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 922d52b6cfe5a..f88709e6e95ac 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -21,6 +21,8 @@ import { useGlobalFullScreen } from '../../../common/containers/use_full_screen' import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; @@ -96,6 +98,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ defaultModel={eventsDefaultModel} end={endDate} id={TimelineId.hostsPageEvents} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} pageFilters={pageFilters} diff --git a/x-pack/plugins/security_solution/public/hosts/store/actions.ts b/x-pack/plugins/security_solution/public/hosts/store/actions.ts index 0416a00be78b6..ab4033ebe7f5a 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/actions.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/actions.ts @@ -6,8 +6,7 @@ */ import actionCreatorFactory from 'typescript-fsa'; - -import { HostsSortField } from '../../graphql/types'; +import { HostsSortField } from '../../../common/search_strategy/security_solution/hosts'; import { HostsTableType, HostsType } from './model'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/hosts'); diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts index 320df452af569..c9dcc3a60b4a9 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { Direction, HostsFields } from '../../graphql/types'; import { DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; import { HostsModel, HostsTableType, HostsType } from './model'; import { setHostsQueriesActivePageToZero } from './helpers'; +import { Direction, HostsFields } from '../../../common/search_strategy'; export const mockHostsState: HostsModel = { page: { diff --git a/x-pack/plugins/security_solution/public/hosts/store/model.ts b/x-pack/plugins/security_solution/public/hosts/store/model.ts index 4c3f90879d5e5..b610971f70305 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/model.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/model.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { Direction, HostsFields } from '../../graphql/types'; +import { HostsFields } from '../../../common/search_strategy/security_solution/hosts'; +import { Direction } from '../../graphql/types'; export enum HostsType { page = 'page', diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index d35117e3533c9..eebf3ca1684a1 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -6,8 +6,8 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { Direction, HostsFields } from '../../../common/search_strategy'; -import { Direction, HostsFields } from '../../graphql/types'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; import { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 8a1b1ccfa5173..f9fc5f32aa63a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -862,32 +862,26 @@ describe('When on the Trusted Apps Page', () => { }); describe('and the search is dispatched', () => { - const renderWithListData = async () => { - const result = render(); + let renderResult: ReturnType; + beforeEach(async () => { + mockListApis(coreStart.http); + reactTestingLibrary.act(() => { + history.push('/trusted_apps?filter=test'); + }); + renderResult = render(); await act(async () => { await waitForAction('trustedAppsListResourceStateChanged'); }); - return result; - }; - - beforeEach(() => mockListApis(coreStart.http)); + }); - it('search bar is filled with query params', async () => { - reactTestingLibrary.act(() => { - history.push('/trusted_apps?filter=test'); - }); - const result = await renderWithListData(); - expect(result.getByDisplayValue('test')).not.toBeNull(); + it('search bar is filled with query params', () => { + expect(renderResult.getByDisplayValue('test')).not.toBeNull(); }); it('search action is dispatched', async () => { - reactTestingLibrary.act(() => { - history.push('/trusted_apps?filter=test'); - }); - const result = await renderWithListData(); await act(async () => { - fireEvent.click(result.getByTestId('trustedAppSearchButton')); - await waitForAction('userChangedUrl'); + fireEvent.click(renderResult.getByTestId('trustedAppSearchButton')); + expect(await waitForAction('userChangedUrl')).not.toBeNull(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap index 8db9006da6156..1b91d396bee30 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap @@ -27,16 +27,16 @@ exports[`ToolTipFilter renders correctly against snapshot 1`] = ` aria-label="Next" color="text" data-test-subj="previous-feature-button" - disabled={true} iconType="arrowLeft" + isDisabled={true} onClick={[MockFunction]} /> diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx index a74022a222528..cc7662cf1e960 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx @@ -42,7 +42,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(true); }); @@ -70,9 +70,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - false - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(false); }); test('nextFeature is called when featureIndex is < totalFeatures', () => { @@ -102,7 +102,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(false); }); @@ -130,9 +130,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - true - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(true); }); test('nextFunction is not called when featureIndex >== totalFeatures', () => { @@ -161,7 +161,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(true); }); @@ -189,9 +189,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - true - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(true); }); test('nextFunction is not called when only a single feature is provided', () => { @@ -221,7 +221,7 @@ describe('ToolTipFilter', () => { ); expect( - wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('disabled') + wrapper.find('[data-test-subj="previous-feature-button"]').first().prop('isDisabled') ).toBe(false); }); @@ -249,9 +249,9 @@ describe('ToolTipFilter', () => { /> ); - expect(wrapper.find('[data-test-subj="next-feature-button"]').first().prop('disabled')).toBe( - false - ); + expect( + wrapper.find('[data-test-subj="next-feature-button"]').first().prop('isDisabled') + ).toBe(false); }); test('nextFunction is called when featureIndex > 0 && featureIndex < totalFeatures', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx index 252260b2c5a2b..dbb280228e504 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx @@ -54,7 +54,7 @@ export const ToolTipFooterComponent = ({ onClick={previousFeature} iconType="arrowLeft" aria-label="Next" - disabled={featureIndex <= 0} + isDisabled={featureIndex <= 0} /> = totalFeatures - 1} + isDisabled={featureIndex >= totalFeatures - 1} /> diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx index 9c97087666198..b43d5af029ec4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -13,7 +13,7 @@ import '../../../../common/mock/react_beautiful_dnd'; import { TestProviders } from '../../../../common/mock'; import { EndpointOverview } from './index'; -import { HostPolicyResponseActionStatus } from '../../../../graphql/types'; +import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts'; describe('EndpointOverview Component', () => { test('it renders with endpoint data', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx index 4caf854278cc2..1b05b600c8e3e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx @@ -13,9 +13,11 @@ import { OverviewDescriptionList } from '../../../../common/components/overview_ import { DescriptionList } from '../../../../../common/utility_types'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; -import { EndpointFields, HostPolicyResponseActionStatus } from '../../../../graphql/types'; - import * as i18n from './translations'; +import { + EndpointFields, + HostPolicyResponseActionStatus, +} from '../../../../../common/search_strategy/security_solution/hosts'; interface Props { contextID?: string; diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 032b500b45fb3..757191fdb54ec 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -58,8 +58,5 @@ export { UseExceptionListItemsSuccess, addEndpointExceptionList, withOptionalSignal, - BuilderEntryItem, - BuilderAndBadgeComponent, - BuilderEntryDeleteButtonComponent, - BuilderExceptionListItemComponent, + ExceptionBuilder, } from '../../lists/public'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index e63ffedf3da7c..459706de36569 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -14,6 +14,8 @@ import { StatefulTimeline } from '../../timeline'; import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; +import { defaultRowRenderers } from '../../timeline/body/renderers'; +import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer'; import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { @@ -46,7 +48,11 @@ const FlyoutPaneComponent: React.FC = ({ timelineId }) onClose={handleClose} size="l" > - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap index e42b5263189dc..84611e0b7f02c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap @@ -1,669 +1,1067 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Expandable Host Component ExpandableHostDetails: rendering it should render the HostOverview of the ExpandableHostDetails 1`] = ` +.c3 { + color: #535966; +} + +.c0 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c0 > * { + max-width: 100%; +} + +.c0 .inspectButtonComponent { + pointer-events: none; + opacity: 0; + -webkit-transition: opacity 250ms ease; + transition: opacity 250ms ease; +} + +.c0:hover .inspectButtonComponent { + pointer-events: auto; + opacity: 1; +} + +.c4 { + padding: 16px; + background: rgba(250,251,253,0.9); + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 1000; +} + +.c5 { + height: 100%; +} + +.c2 dt { + font-size: 12px !important; +} + +.c2 dd { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c2 dd > div { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c1 { + position: relative; +} + +.c1 .euiButtonIcon { + position: absolute; + right: 12px; + top: 6px; + z-index: 2; +} + - - - +
    + + +
    + + — + , + "title": "Host ID", }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "id", + Object { + "description": + — + , + "title": "First seen", + }, + Object { + "description": + — + , + "title": "Last seen", + }, + ] + } + key="0" + > + +
    + + — + , + "title": "Host ID", }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [ + Object { + "description": + — + , + "title": "First seen", + }, + Object { + "description": + — + , + "title": "Last seen", + }, + ] + } + > + + — + , + "title": "Host ID", }, Object { - "kind": "Argument", - "name": Object { - "kind": "Name", - "value": "timerange", - }, - "value": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "timerange", - }, - }, + "description": + — + , + "title": "First seen", }, Object { - "kind": "Argument", - "name": Object { - "kind": "Name", - "value": "defaultIndex", - }, - "value": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "defaultIndex", - }, - }, + "description": + — + , + "title": "Last seen", }, - ], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "HostOverview", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "_id", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "agent", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "id", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "host", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "architecture", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "id", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "ip", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "mac", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "name", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "os", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "family", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "name", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "platform", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "version", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "type", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "cloud", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "instance", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "id", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "machine", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "type", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "provider", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "region", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [ - Object { - "arguments": Array [ - Object { - "kind": "Argument", - "name": Object { - "kind": "Name", - "value": "if", - }, - "value": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "inspect", - }, - }, - }, - ], - "kind": "Directive", - "name": Object { - "kind": "Name", - "value": "include", - }, - }, - ], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "inspect", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "dsl", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "response", - }, - "selectionSet": undefined, - }, - ], - }, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "endpoint", - }, - "selectionSet": Object { - "kind": "SelectionSet", - "selections": Array [ - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "endpointPolicy", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "policyStatus", - }, - "selectionSet": undefined, - }, - Object { - "alias": undefined, - "arguments": Array [], - "directives": Array [], - "kind": "Field", - "name": Object { - "kind": "Name", - "value": "sensorVersion", - }, - "selectionSet": undefined, - }, - ], - }, - }, - ], - }, - }, - ], + ] + } + > +
    + +
    + Host ID +
    +
    + +
    + + + — + + +
    +
    + +
    + First seen +
    +
    + +
    + + + — + + +
    +
    + +
    + Last seen +
    +
    + +
    + + + — + + +
    +
    +
    +
    +
    +
    +
    +
    + , + "title": "IP addresses", }, - }, - ], - }, - "variableDefinitions": Array [ - Object { - "defaultValue": undefined, - "kind": "VariableDefinition", - "type": Object { - "kind": "NonNullType", - "type": Object { - "kind": "NamedType", - "name": Object { - "kind": "Name", - "value": "ID", - }, + Object { + "description": , + "title": "MAC addresses", }, - }, - "variable": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "sourceId", + Object { + "description": , + "title": "Platform", }, - }, - }, - Object { - "defaultValue": undefined, - "kind": "VariableDefinition", - "type": Object { - "kind": "NonNullType", - "type": Object { - "kind": "NamedType", - "name": Object { - "kind": "Name", - "value": "String", - }, + ] + } + key="1" + > + +
    + , + "title": "IP addresses", + }, + Object { + "description": , + "title": "MAC addresses", + }, + Object { + "description": , + "title": "Platform", + }, + ] + } + > + , + "title": "IP addresses", + }, + Object { + "description": , + "title": "MAC addresses", + }, + Object { + "description": , + "title": "Platform", + }, + ] + } + > +
    + +
    + IP addresses +
    +
    + +
    + + + + — + + + +
    +
    + +
    + MAC addresses +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Platform +
    +
    + +
    + + + + — + + + +
    +
    +
    +
    +
    +
    +
    +
    + , + "title": "Operating system", }, - }, - "variable": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "hostName", + Object { + "description": , + "title": "Family", }, - }, - }, - Object { - "defaultValue": undefined, - "kind": "VariableDefinition", - "type": Object { - "kind": "NonNullType", - "type": Object { - "kind": "NamedType", - "name": Object { - "kind": "Name", - "value": "TimerangeInput", - }, + Object { + "description": , + "title": "Version", }, - }, - "variable": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "timerange", + Object { + "description": , + "title": "Architecture", }, - }, - }, - Object { - "defaultValue": undefined, - "kind": "VariableDefinition", - "type": Object { - "kind": "NonNullType", - "type": Object { - "kind": "ListType", - "type": Object { - "kind": "NonNullType", - "type": Object { - "kind": "NamedType", - "name": Object { - "kind": "Name", - "value": "String", + ] + } + key="2" + > + +
    + , + "title": "Operating system", + }, + Object { + "description": , + "title": "Family", + }, + Object { + "description": , + "title": "Version", + }, + Object { + "description": , + "title": "Architecture", }, - }, - }, + ] + } + > + , + "title": "Operating system", + }, + Object { + "description": , + "title": "Family", + }, + Object { + "description": , + "title": "Version", + }, + Object { + "description": , + "title": "Architecture", + }, + ] + } + > +
    + +
    + Operating system +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Family +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Version +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Architecture +
    +
    + +
    + + + + — + + + +
    +
    +
    +
    +
    +
    +
    +
    + , + "title": "Cloud provider", }, - }, - "variable": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "defaultIndex", + Object { + "description": , + "title": "Region", }, - }, - }, - Object { - "defaultValue": undefined, - "kind": "VariableDefinition", - "type": Object { - "kind": "NonNullType", - "type": Object { - "kind": "NamedType", - "name": Object { - "kind": "Name", - "value": "Boolean", - }, + Object { + "description": , + "title": "Instance ID", }, - }, - "variable": Object { - "kind": "Variable", - "name": Object { - "kind": "Name", - "value": "inspect", + Object { + "description": , + "title": "Machine type", }, - }, - }, - ], - }, - ], - "kind": "Document", - "loc": Object { - "end": 930, - "start": 0, - }, - } - } - skip={false} - variables={ - Object { - "defaultIndex": Array [ - "IShouldBeUsed", - ], - "hostName": "testHostName", - "inspect": false, - "sourceId": "default", - "timerange": Object { - "from": "2020-07-07T08:20:18.966Z", - "interval": "12h", - "to": "2020-07-08T08:20:18.966Z", - }, - } - } - /> - - + ] + } + key="3" + > + +
    + , + "title": "Cloud provider", + }, + Object { + "description": , + "title": "Region", + }, + Object { + "description": , + "title": "Instance ID", + }, + Object { + "description": , + "title": "Machine type", + }, + ] + } + > + , + "title": "Cloud provider", + }, + Object { + "description": , + "title": "Region", + }, + Object { + "description": , + "title": "Instance ID", + }, + Object { + "description": , + "title": "Machine type", + }, + ] + } + > +
    + +
    + Cloud provider +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Region +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Instance ID +
    +
    + +
    + + + + — + + + +
    +
    + +
    + Machine type +
    +
    + +
    + + + + — + + + +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + + +
    `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx index 2ce7090a5b83a..a9ab89359d0ae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx @@ -70,9 +70,7 @@ describe('Expandable Host Component', () => { ); - expect(wrapper.find('HostOverviewByNameComponentQuery').prop('indexNames')).toStrictEqual([ - 'IShouldBeUsed', - ]); + expect(wrapper.find('HostOverview').prop('indexNames')).toStrictEqual(['IShouldBeUsed']); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx index 78367d17d7b62..f18f1eb993ee2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx @@ -19,7 +19,7 @@ import { HostItem } from '../../../../../common/search_strategy'; import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/anomaly_table_provider'; import { hostToCriteria } from '../../../../common/components/ml/criteria/host_to_criteria'; import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; -import { HostOverviewByNameQuery } from '../../../../hosts/containers/hosts/details'; +import { useHostDetails, ID } from '../../../../hosts/containers/hosts/details'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; interface ExpandableHostProps { @@ -71,48 +71,42 @@ export const ExpandableHostDetails = ({ [] ); const allPatterns = useDeepEqualSelector(allExistingIndexNamesSelector); - + const [loading, { hostDetails: hostOverview }] = useHostDetails({ + endDate: to, + hostName, + indexNames: allPatterns, + startDate: from, + }); return ( - - {({ hostOverview, loading, id }) => ( - ( + - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - + narrowDateRange={(score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 72d2956bd4086..91d039a19495c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -22,26 +22,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 2

    - @@ -63,15 +114,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 3

    - @@ -93,15 +206,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 4

    - @@ -123,15 +298,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 5

    - @@ -153,15 +390,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 6

    - @@ -183,15 +482,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 7

    - @@ -213,15 +574,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 8

    - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index f20978c6ba726..234e28e6231c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -9,10 +9,10 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import '../../../../../common/mock/match_media'; import { mockTimelineData } from '../../../../../common/mock'; import { defaultHeaders } from '../column_headers/default_headers'; -import { columnRenderers } from '../renderers'; import { DataDrivenColumns } from '.'; @@ -25,11 +25,11 @@ describe('Columns', () => { ariaRowindex={2} _id={mockTimelineData[0]._id} columnHeaders={headersSansTimestamp} - columnRenderers={columnRenderers} data={mockTimelineData[0].data} ecsData={mockTimelineData[0].ecs} hasRowRenderers={false} notesCount={0} + renderCellValue={DefaultCellRenderer} timelineId="test" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 5aba562749f01..aeb9af46ea2ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -9,6 +9,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; import { getOr } from 'lodash/fp'; +import { CellValueElementProps } from '../../cell_rendering'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -16,20 +17,19 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { EventsTd, EVENTS_TD_CLASS_NAME, EventsTdContent, EventsTdGroupData } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { getColumnRenderer } from '../renderers/get_column_renderer'; +import { StatefulCell } from './stateful_cell'; import * as i18n from './translations'; interface Props { _id: string; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; hasRowRenderers: boolean; notesCount: number; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; tabType?: TimelineTabs; timelineId: string; } @@ -82,11 +82,11 @@ export const DataDrivenColumns = React.memo( _id, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, hasRowRenderers, notesCount, + renderCellValue, tabType, timelineId, }) => ( @@ -105,18 +105,16 @@ export const DataDrivenColumns = React.memo(

    {i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: i + 2 })}

    - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} + {hasRowRenderers ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx new file mode 100644 index 0000000000000..3c75bc7fb2649 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React, { useEffect } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../store/timeline/model'; + +import { StatefulCell } from './stateful_cell'; +import { getMappedNonEcsValue } from '.'; + +/** + * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, + * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid + * + * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. + * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, + * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: + * https://codesandbox.io/s/zhxmo + */ +const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => { + useEffect(() => { + // branching logic that conditionally renders a specific cell green: + if (columnId === defaultHeaders[0].id) { + const value = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + + if (value?.length) { + setCellProps({ + style: { + backgroundColor: 'green', + }, + }); + } + } + }, [columnId, data, setCellProps]); + + return ( +
    + {getMappedNonEcsValue({ + data, + fieldName: columnId, + })} +
    + ); +}; + +describe('StatefulCell', () => { + const ariaRowindex = 123; + const eventId = '_id-123'; + const linkValues = ['foo', 'bar', '@baz']; + const tabType = TimelineTabs.query; + const timelineId = 'test'; + + let header: ColumnHeaderOptions; + let data: TimelineNonEcsData[]; + beforeEach(() => { + data = cloneDeep(mockTimelineData[0].data); + header = cloneDeep(defaultHeaders[0]); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId: `${timelineId}-${tabType}`, + }) + ); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId, + }) + ); + }); + + test('it renders the React.Node returned by renderCellValue', () => { + const renderCellValue = () =>
    ; + + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); + }); + + test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') + ).toEqual('background-color: green;'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx new file mode 100644 index 0000000000000..83f603364ba8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { HTMLAttributes, useState } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; + +export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; +} + +const StatefulCellComponent = ({ + ariaRowindex, + data, + header, + eventId, + linkValues, + renderCellValue, + tabType, + timelineId, +}: { + ariaRowindex: number; + data: TimelineNonEcsData[]; + header: ColumnHeaderOptions; + eventId: string; + linkValues: string[] | undefined; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; +}) => { + const [cellProps, setCellProps] = useState>({}); + + return ( +
    + {renderCellValue({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + setCellProps, + timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, + })} +
    + ); +}; + +StatefulCellComponent.displayName = 'StatefulCellComponent'; + +export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index abdfda3272d6a..74724dedf4d11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -14,6 +14,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import * as i18n from '../translations'; import { EventColumnView } from './event_column_view'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; @@ -56,6 +57,7 @@ describe('EventColumnView', () => { onRowSelected: jest.fn(), onUnPinEvent: jest.fn(), refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, selectedEventIds: {}, showCheckboxes: false, showNotes: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c6caf0a7b5b15..a0a0aeb23e8f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo } from 'react'; +import { CellValueElementProps } from '../../cell_rendering'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -21,7 +22,6 @@ import { getPinOnClick, InvestigateInResolverAction, } from '../helpers'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { AddEventNoteAction } from '../actions/add_note_icon_item'; @@ -38,7 +38,6 @@ interface Props { actionsColumnWidth: number; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; eventIdToNoteIds: Readonly>; @@ -51,6 +50,7 @@ interface Props { onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; hasRowRenderers: boolean; selectedEventIds: Readonly>; @@ -69,7 +69,6 @@ export const EventColumnView = React.memo( actionsColumnWidth, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, eventIdToNoteIds, @@ -84,6 +83,7 @@ export const EventColumnView = React.memo( refetch, hasRowRenderers, onRuleChange, + renderCellValue, selectedEventIds, showCheckboxes, showNotes, @@ -227,11 +227,11 @@ export const EventColumnView = React.memo( _id={id} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} ecsData={ecsData} hasRowRenderers={hasRowRenderers} notesCount={notesCount} + renderCellValue={renderCellValue} tabType={tabType} timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index d76b5834c233e..7f8a3a92fb5ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { isEmpty } from 'lodash'; +import { CellValueElementProps } from '../../cell_rendering'; import { inputsModel } from '../../../../../common/store'; import { BrowserFields } from '../../../../../common/containers/source'; import { @@ -18,7 +19,6 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { StatefulEvent } from './stateful_event'; import { eventIsPinned } from '../helpers'; @@ -30,7 +30,6 @@ interface Props { actionsColumnWidth: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; containerRef: React.MutableRefObject; data: TimelineItem[]; eventIdToNoteIds: Readonly>; @@ -41,6 +40,7 @@ interface Props { onRowSelected: OnRowSelected; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; @@ -52,7 +52,6 @@ const EventsComponent: React.FC = ({ actionsColumnWidth, browserFields, columnHeaders, - columnRenderers, containerRef, data, eventIdToNoteIds, @@ -64,6 +63,7 @@ const EventsComponent: React.FC = ({ pinnedEventIds, refetch, onRuleChange, + renderCellValue, rowRenderers, selectedEventIds, showCheckboxes, @@ -76,7 +76,6 @@ const EventsComponent: React.FC = ({ ariaRowindex={i + ARIA_ROW_INDEX_OFFSET} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} containerRef={containerRef} event={event} eventIdToNoteIds={eventIdToNoteIds} @@ -88,6 +87,7 @@ const EventsComponent: React.FC = ({ lastFocusedAriaColindex={lastFocusedAriaColindex} loadingEventIds={loadingEventIds} onRowSelected={onRowSelected} + renderCellValue={renderCellValue} refetch={refetch} rowRenderers={rowRenderers} onRuleChange={onRuleChange} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 4191badd6b03f..97ab088b61583 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { CellValueElementProps } from '../../cell_rendering'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineExpandedDetailType, @@ -23,7 +24,6 @@ import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/mod import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; @@ -45,7 +45,6 @@ interface Props { containerRef: React.MutableRefObject; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; isEventViewer?: boolean; @@ -56,6 +55,7 @@ interface Props { refetch: inputsModel.Refetch; ariaRowindex: number; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -77,7 +77,6 @@ const StatefulEventComponent: React.FC = ({ browserFields, containerRef, columnHeaders, - columnRenderers, event, eventIdToNoteIds, isEventViewer = false, @@ -86,8 +85,9 @@ const StatefulEventComponent: React.FC = ({ loadingEventIds, onRowSelected, refetch, - onRuleChange, + renderCellValue, rowRenderers, + onRuleChange, ariaRowindex, selectedEventIds, showCheckboxes, @@ -259,7 +259,6 @@ const StatefulEventComponent: React.FC = ({ actionsColumnWidth={actionsColumnWidth} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={event.data} ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} @@ -273,6 +272,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} refetch={refetch} + renderCellValue={renderCellValue} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 723e4c3de5c27..76dbfc553d228 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import '../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { Direction } from '../../../../../common/search_strategy'; @@ -19,6 +20,7 @@ import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; +import { defaultRowRenderers } from './renderers'; const mockSort: Sort[] = [ { @@ -39,8 +41,8 @@ jest.mock('react-redux', () => { }); jest.mock('../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), - useDeepEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), + useShallowEqualSelector: () => mockTimelineModel, + useDeepEqualSelector: () => mockTimelineModel, })); jest.mock('../../../../common/components/link_to'); @@ -76,6 +78,8 @@ describe('Body', () => { loadingEventIds: [], pinnedEventIds: {}, refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, selectedEventIds: {}, setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], sort: mockSort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 4df6eb16ccb62..59c0610c544e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { CellValueElementProps } from '../cell_rendering'; import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { FIRST_ARIA_INDEX, @@ -28,9 +29,9 @@ import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; import { getEventIdToDataMapping } from './helpers'; -import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { plainRowRenderer } from './renderers/plain_row_renderer'; +import { RowRenderer } from './renderers/row_renderer'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { Events } from './events'; @@ -44,6 +45,8 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort[]; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; tabType: TimelineTabs; totalPages: number; onRuleChange?: () => void; @@ -83,6 +86,8 @@ export const BodyComponent = React.memo( onRuleChange, showCheckboxes, refetch, + renderCellValue, + rowRenderers, sort, tabType, totalPages, @@ -141,7 +146,7 @@ export const BodyComponent = React.memo( if (!excludedRowRendererIds) return rowRenderers; return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds]); + }, [excludedRowRendererIds, rowRenderers]); const actionsColumnWidth = useMemo( () => @@ -209,7 +214,6 @@ export const BodyComponent = React.memo( actionsColumnWidth={actionsColumnWidth} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} eventIdToNoteIds={eventIdToNoteIds} id={id} @@ -219,6 +223,7 @@ export const BodyComponent = React.memo( onRowSelected={onRowSelected} pinnedEventIds={pinnedEventIds} refetch={refetch} + renderCellValue={renderCellValue} rowRenderers={enabledRowRenderers} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} @@ -244,6 +249,8 @@ export const BodyComponent = React.memo( prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.tabType === nextProps.tabType ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 6e36102da2de9..b92a4381d837b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -17,7 +17,7 @@ import { mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; -import { rowRenderers } from '.'; +import { defaultRowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; jest.mock('@elastic/eui', () => { @@ -48,7 +48,7 @@ describe('get_column_renderer', () => { }); test('renders correctly against snapshot', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -60,7 +60,7 @@ describe('get_column_renderer', () => { }); test('should render plain row data when it is a non suricata row', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -75,7 +75,7 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data when it is a suricata row', () => { - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -93,7 +93,7 @@ describe('get_column_renderer', () => { test('should render a suricata row data if event.category is network_traffic', () => { suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -111,7 +111,7 @@ describe('get_column_renderer', () => { test('should render a zeek row data if event.category is network_traffic', () => { zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(zeek, rowRenderers); + const rowRenderer = getRowRenderer(zeek, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, @@ -129,7 +129,7 @@ describe('get_column_renderer', () => { test('should render a system row data if event.category is network_traffic', () => { system.event = { ...system.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(system, rowRenderers); + const rowRenderer = getRowRenderer(system, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, @@ -147,7 +147,7 @@ describe('get_column_renderer', () => { test('should render a auditd row data if event.category is network_traffic', () => { auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(auditd, rowRenderers); + const rowRenderer = getRowRenderer(auditd, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 671d183c62e6d..209a9414f62f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -23,7 +23,7 @@ import { systemRowRenderers } from './system/generic_row_renderer'; // Suricata and Zeek which is why Suricata and Zeek are above it. The // plainRowRenderer always returns true to everything which is why it always // should be last. -export const rowRenderers: RowRenderer[] = [ +export const defaultRowRenderers: RowRenderer[] = [ ...auditdRowRenderers, ...systemRowRenderers, suricataRowRenderer, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx new file mode 100644 index 0000000000000..5ac1dcf8805cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; +import { DefaultCellRenderer } from './default_cell_renderer'; + +jest.mock('../body/renderers/get_column_renderer'); +const getColumnRendererMock = getColumnRenderer as jest.Mock; +const mockImplementation = { + renderColumn: jest.fn(), +}; + +describe('DefaultCellRenderer', () => { + const columnId = 'signal.rule.risk_score'; + const eventId = '_id-123'; + const isDetails = true; + const isExpandable = true; + const isExpanded = true; + const linkValues = ['foo', 'bar', '@baz']; + const rowIndex = 3; + const setCellProps = jest.fn(); + const timelineId = 'test'; + + beforeEach(() => { + jest.clearAllMocks(); + getColumnRendererMock.mockImplementation(() => mockImplementation); + }); + + test('it invokes `getColumnRenderer` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data); + }); + + test('it invokes `renderColumn` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(mockImplementation.renderColumn).toBeCalledWith({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: ['2018-11-05T19:03:25.937Z'], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx new file mode 100644 index 0000000000000..8d8f821107e7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { getMappedNonEcsValue } from '../body/data_driven_columns'; +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; + +import { CellValueElementProps } from '.'; + +export const DefaultCellRenderer: React.FC = ({ + columnId, + data, + eventId, + header, + linkValues, + setCellProps, + timelineId, +}) => ( + <> + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx new file mode 100644 index 0000000000000..03e444e3a9afd --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; + +import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; +import { ColumnHeaderOptions } from '../../../store/timeline/model'; + +/** The following props are provided to the function called by `renderCellValue` */ +export type CellValueElementProps = EuiDataGridCellValueElementProps & { + data: TimelineNonEcsData[]; + eventId: string; // _id + header: ColumnHeaderOptions; + linkValues: string[] | undefined; + timelineId: string; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap index 2595f29144b80..7d237ecaf92df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap @@ -140,6 +140,986 @@ In other use cases the message field can be used to concatenate different values ] } onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } showExpandedDetails={false} start="2018-03-23T18:49:23.132Z" timelineId="test" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index 7b77a915f2f05..e13bed1e2eff6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -9,6 +9,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; +import { defaultRowRenderers } from '../body/renderers'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; @@ -94,6 +96,8 @@ describe('Timeline', () => { itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showExpandedDetails: false, start: startDate, timerangeKind: 'absolute', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 51f8db4e796e5..6bb19ce5a6852 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -22,10 +22,12 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { calculateTotalPages } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; @@ -133,6 +135,8 @@ const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.timerangeKind === nextProps.timerangeKind; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -154,6 +158,8 @@ export const EqlTabContentComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, start, timerangeKind, @@ -284,6 +290,8 @@ export const EqlTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={NO_SORTING} tabType={TimelineTabs.eql} totalPages={calculateTotalPages({ 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 ee2ce8cf8103b..db7a3cc3c9900 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 @@ -17,7 +17,9 @@ import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { DefaultCellRenderer } from './cell_rendering/default_cell_renderer'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; +import { defaultRowRenderers } from './body/renderers'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -63,6 +65,8 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, timelineId: TimelineId.test, }; 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 6d2374dd8eef7..367357511c9c8 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 @@ -14,6 +14,8 @@ import styled from 'styled-components'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; +import { RowRenderer } from './body/renderers/row_renderer'; +import { CellValueElementProps } from './cell_rendering'; import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -36,10 +38,12 @@ const TimelineTemplateBadge = styled.div` `; export interface Props { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: TimelineId; } -const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { +const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const isSaving = useShallowEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isSaving @@ -50,7 +54,11 @@ const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { const TimelineSavingProgress = React.memo(TimelineSavingProgressComponent); -const StatefulTimelineComponent: React.FC = ({ timelineId }) => { +const StatefulTimelineComponent: React.FC = ({ + renderCellValue, + rowRenderers, + timelineId, +}) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -131,6 +139,8 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { { timelineId: TimelineId.test, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, sort, pinnedEventIds: {}, showExpandedDetails: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index a19a61d8268ff..dfc14747dacf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -14,10 +14,12 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -87,6 +89,8 @@ const VerticalRule = styled.div` VerticalRule.displayName = 'VerticalRule'; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -106,6 +110,8 @@ export const PinnedTabContentComponent: React.FC = ({ itemsPerPageOptions, pinnedEventIds, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, sort, }) => { @@ -217,6 +223,8 @@ export const PinnedTabContentComponent: React.FC = ({ data={events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.pinned} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index 0688a10b31eef..46c85f634ff6b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -276,6 +276,986 @@ In other use cases the message field can be used to concatenate different values kqlMode="search" kqlQueryExpression="" onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } show={true} showCallOutUnauthorizedMsg={false} showExpandedDetails={false} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index c7d27da64c650..ede473acbfb2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -10,11 +10,13 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { Direction } from '../../../../graphql/types'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from './index'; +import { defaultRowRenderers } from '../body/renderers'; import { Sort } from '../body/sort'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; @@ -106,6 +108,8 @@ describe('Timeline', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, sort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 28fec7ded9ca2..74a0f02354219 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -22,6 +22,8 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction, TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { useKibana } from '../../../../common/lib/kibana'; @@ -142,6 +144,8 @@ const compareQueryProps = (prevProps: Props, nextProps: Props) => deepEqual(prevProps.filters, nextProps.filters); interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -164,6 +168,8 @@ export const QueryTabContentComponent: React.FC = ({ kqlMode, kqlQueryExpression, onEventClosed, + renderCellValue, + rowRenderers, show, showCallOutUnauthorizedMsg, showExpandedDetails, @@ -330,6 +336,8 @@ export const QueryTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index f29211d519841..76a2ad0960322 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -20,6 +20,8 @@ import { TimelineEventsCountBadge, } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { getActiveTabSelector, getNoteIdsSelector, @@ -46,6 +48,8 @@ const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); interface BasicTimelineTab { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; setTimelineFullScreen?: (fullScreen: boolean) => void; timelineFullScreen?: boolean; timelineId: TimelineId; @@ -53,16 +57,32 @@ interface BasicTimelineTab { graphEventId?: string; } -const QueryTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const QueryTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); QueryTab.displayName = 'QueryTab'; -const EqlTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const EqlTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); EqlTab.displayName = 'EqlTab'; @@ -81,9 +101,17 @@ const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => )); NotesTab.displayName = 'NotesTab'; -const PinnedTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const PinnedTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); PinnedTab.displayName = 'PinnedTab'; @@ -91,7 +119,7 @@ PinnedTab.displayName = 'PinnedTab'; type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs }; const ActiveTimelineTab = memo( - ({ activeTimelineTab, timelineId, timelineType }) => { + ({ activeTimelineTab, renderCellValue, rowRenderers, timelineId, timelineType }) => { const getTab = useCallback( (tab: TimelineTabs) => { switch (tab) { @@ -119,14 +147,26 @@ const ActiveTimelineTab = memo( return ( <> - + - + {timelineType === TimelineType.default && ( - + )} @@ -160,6 +200,8 @@ const StyledEuiTab = styled(EuiTab)` `; const TabsContentComponent: React.FC = ({ + renderCellValue, + rowRenderers, timelineId, timelineFullScreen, timelineType, @@ -300,6 +342,8 @@ const TabsContentComponent: React.FC = ({ 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 3d92397f4ab50..0b70ba8991686 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 @@ -30,11 +30,12 @@ import { updateItemsPerPage, updateSort, } from './actions'; - +import { DefaultCellRenderer } from '../../components/timeline/cell_rendering/default_cell_renderer'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps, } from '../../components/timeline/query_tab_content'; +import { defaultRowRenderers } from '../../components/timeline/body/renderers'; import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction } from '../../../graphql/types'; @@ -90,6 +91,8 @@ describe('epicLocalStorage', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, start: startDate, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts new file mode 100644 index 0000000000000..93af1f406300c --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { run, RunFn, createFailError } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; +import { AxiosError } from 'axios'; +import bluebird from 'bluebird'; +import { EventFilterGenerator } from '../../../common/endpoint/data_generators/event_filter_generator'; +import { + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../lists/common/constants'; +import { CreateExceptionListSchema } from '../../../../lists/common'; + +export const cli = () => { + run( + async (options) => { + try { + await createEventFilters(options); + options.log.success(`${options.flags.count} endpoint event filters created`); + } catch (e) { + options.log.error(e); + throw createFailError(e.message); + } + }, + { + description: 'Load Endpoint Event Filters', + flags: { + string: ['kibana'], + default: { + count: 10, + kibana: 'http://elastic:changeme@localhost:5601', + }, + help: ` + --count Number of event filters to create. Default: 10 + --kibana The URL to kibana including credentials. Default: http://elastic:changeme@localhost:5601 + `, + }, + } + ); +}; + +class EventFilterDataLoaderError extends Error { + constructor(message: string, public readonly meta: unknown) { + super(message); + } +} + +const handleThrowAxiosHttpError = (err: AxiosError): never => { + let message = err.message; + + if (err.response) { + message = `[${err.response.status}] ${err.response.data.message ?? err.message} [ ${String( + err.response.config.method + ).toUpperCase()} ${err.response.config.url} ]`; + } + throw new EventFilterDataLoaderError(message, err.toJSON()); +}; + +const createEventFilters: RunFn = async ({ flags, log }) => { + const eventGenerator = new EventFilterGenerator(); + const kbn = new KbnClient({ log, url: flags.kibana as string }); + + await ensureCreateEndpointEventFiltersList(kbn); + + await bluebird.map( + Array.from({ length: (flags.count as unknown) as number }), + () => + kbn + .request({ + method: 'POST', + path: EXCEPTION_LIST_ITEM_URL, + body: eventGenerator.generate(), + }) + .catch((e) => handleThrowAxiosHttpError(e)), + { concurrency: 10 } + ); +}; + +const ensureCreateEndpointEventFiltersList = async (kbn: KbnClient) => { + const newListDefinition: CreateExceptionListSchema = { + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + meta: undefined, + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + os_types: [], + tags: [], + type: 'endpoint', + namespace_type: 'agnostic', + }; + + await kbn + .request({ + method: 'POST', + path: EXCEPTION_LIST_URL, + body: newListDefinition, + }) + .catch((e) => { + // Ignore if list was already created + if (e.response.status !== 409) { + handleThrowAxiosHttpError(e); + } + }); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js b/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js new file mode 100755 index 0000000000000..ca0f4ff9365c5 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../src/setup_node_env'); +require('./event_filters').cli(); diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts deleted file mode 100644 index cc867a3a31463..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts +++ /dev/null @@ -1,80 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getOr } from 'lodash/fp'; - -import { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { - Hosts, - HostOverviewRequestOptions, - HostsRequestOptions, - HostLastFirstSeenRequestOptions, -} from '../../lib/hosts'; -import { getFields } from '../../utils/build_query'; -import { createOptionsPaginated } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -type QueryHostsResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -type QueryHostOverviewResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -type QueryHostFirstLastSeenResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface HostsResolversDeps { - hosts: Hosts; -} - -export const createHostsResolvers = ( - libs: HostsResolversDeps -): { - Source: { - Hosts: QueryHostsResolver; - HostOverview: QueryHostOverviewResolver; - HostFirstLastSeen: QueryHostFirstLastSeenResolver; - }; -} => ({ - Source: { - async Hosts(source, args, { req }, info) { - const options: HostsRequestOptions = { - ...createOptionsPaginated(source, args, info), - sort: args.sort, - defaultIndex: args.defaultIndex, - }; - return libs.hosts.getHosts(req, options); - }, - async HostOverview(source, args, { req }, info) { - const fields = getFields(getOr([], 'fieldNodes[0]', info)); - const options: HostOverviewRequestOptions = { - defaultIndex: args.defaultIndex, - sourceConfiguration: source.configuration, - fields: fields.map((field) => field.replace('edges.node.', '')), - hostName: args.hostName, - timerange: args.timerange, - }; - return libs.hosts.getHostOverview(req, options); - }, - async HostFirstLastSeen(source, args, { req }) { - const options: HostLastFirstSeenRequestOptions = { - sourceConfiguration: source.configuration, - hostName: args.hostName, - defaultIndex: args.defaultIndex, - docValueFields: args.docValueFields, - }; - return libs.hosts.getHostFirstLastSeen(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/index.ts b/x-pack/plugins/security_solution/server/graphql/index.ts index baef73b8a8b0d..ba3a1371f1829 100644 --- a/x-pack/plugins/security_solution/server/graphql/index.ts +++ b/x-pack/plugins/security_solution/server/graphql/index.ts @@ -9,7 +9,6 @@ import { rootSchema } from '../../common/graphql/root'; import { sharedSchema } from '../../common/graphql/shared'; import { ecsSchema } from './ecs'; -import { hostsSchema } from './hosts'; import { dateSchema } from './scalar_date'; import { noteSchema } from './note'; import { pinnedEventSchema } from './pinned_event'; @@ -27,7 +26,6 @@ export const schemas = [ toNumberSchema, toDateSchema, toBooleanSchema, - hostsSchema, noteSchema, pinnedEventSchema, rootSchema, diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 06476616d4a61..29d366e20c299 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -23,38 +23,6 @@ export interface SortNote { sortOrder: Direction; } -export interface TimerangeInput { - /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ - interval: string; - /** The end of the timerange */ - to: string; - /** The beginning of the timerange */ - from: string; -} - -export interface PaginationInputPaginated { - /** The activePage parameter defines the page of results you want to fetch */ - activePage: number; - /** The cursorStart parameter defines the start of the results to be displayed */ - cursorStart: number; - /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ - fakePossibleCount: number; - /** The querySize parameter is the number of items to be returned */ - querySize: number; -} - -export interface HostsSortField { - field: HostsFields; - - direction: Direction; -} - -export interface DocValueFieldsInput { - field: string; - - format: string; -} - export interface PageInfoTimeline { pageIndex: number; @@ -247,6 +215,21 @@ export interface SortTimelineInput { sortDirection?: Maybe; } +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface DocValueFieldsInput { + field: string; + + format: string; +} + export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -256,6 +239,17 @@ export interface PaginationInput { tiebreaker?: Maybe; } +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + export interface SortField { sortFieldId: string; @@ -280,18 +274,6 @@ export enum Direction { desc = 'desc', } -export enum HostsFields { - hostName = 'hostName', - lastSeen = 'lastSeen', -} - -export enum HostPolicyResponseActionStatus { - success = 'success', - failure = 'failure', - warning = 'warning', - unsupported = 'unsupported', -} - export enum TimelineType { default = 'default', template = 'template', @@ -351,12 +333,10 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export type ToStringArray = string[] | string; - -export type Date = string; - export type ToAny = any; +export type ToStringArray = string[] | string; + export type ToStringArrayNoNullable = any; export type ToDateArray = string[] | string; @@ -365,6 +345,8 @@ export type ToNumberArray = number[] | number; export type ToBooleanArray = boolean[] | boolean; +export type Date = string; + export type ToIFieldSubTypeNonNullable = any; // ==================================================== @@ -454,12 +436,6 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ - Hosts: HostsData; - - HostOverview: HostItem; - - HostFirstLastSeen: FirstLastSeenHost; } /** A set of configuration options for a security data source */ @@ -492,126 +468,6 @@ export interface SourceStatus { indexFields: string[]; } -export interface HostsData { - edges: HostsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface HostsEdges { - node: HostItem; - - cursor: CursorType; -} - -export interface HostItem { - _id?: Maybe; - - agent?: Maybe; - - cloud?: Maybe; - - endpoint?: Maybe; - - host?: Maybe; - - inspect?: Maybe; - - lastSeen?: Maybe; -} - -export interface AgentFields { - id?: Maybe; -} - -export interface CloudFields { - instance?: Maybe; - - machine?: Maybe; - - provider?: Maybe<(Maybe)[]>; - - region?: Maybe<(Maybe)[]>; -} - -export interface CloudInstance { - id?: Maybe<(Maybe)[]>; -} - -export interface CloudMachine { - type?: Maybe<(Maybe)[]>; -} - -export interface EndpointFields { - endpointPolicy?: Maybe; - - sensorVersion?: Maybe; - - policyStatus?: Maybe; -} - -export interface HostEcsFields { - architecture?: Maybe; - - id?: Maybe; - - ip?: Maybe; - - mac?: Maybe; - - name?: Maybe; - - os?: Maybe; - - type?: Maybe; -} - -export interface OsEcsFields { - platform?: Maybe; - - name?: Maybe; - - full?: Maybe; - - family?: Maybe; - - version?: Maybe; - - kernel?: Maybe; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface CursorType { - value?: Maybe; - - tiebreaker?: Maybe; -} - -export interface PageInfoPaginated { - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; -} - -export interface FirstLastSeenHost { - inspect?: Maybe; - - firstSeen?: Maybe; - - lastSeen?: Maybe; -} - export interface TimelineResult { columns?: Maybe; @@ -987,6 +843,36 @@ export interface AuditdEcsFields { sequence?: Maybe; } +export interface OsEcsFields { + platform?: Maybe; + + name?: Maybe; + + full?: Maybe; + + family?: Maybe; + + version?: Maybe; + + kernel?: Maybe; +} + +export interface HostEcsFields { + architecture?: Maybe; + + id?: Maybe; + + ip?: Maybe; + + mac?: Maybe; + + name?: Maybe; + + os?: Maybe; + + type?: Maybe; +} + export interface Thread { id?: Maybe; @@ -1549,34 +1435,10 @@ export interface EcsEdges { cursor: CursorType; } -export interface OsFields { - platform?: Maybe; - - name?: Maybe; - - full?: Maybe; - - family?: Maybe; - - version?: Maybe; - - kernel?: Maybe; -} - -export interface HostFields { - architecture?: Maybe; - - id?: Maybe; - - ip?: Maybe<(Maybe)[]>; - - mac?: Maybe<(Maybe)[]>; - - name?: Maybe; - - os?: Maybe; +export interface CursorType { + value?: Maybe; - type?: Maybe; + tiebreaker?: Maybe; } /** A descriptor of a field in an index */ @@ -1611,6 +1473,20 @@ export interface PageInfo { hasNextPage?: Maybe; } +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface PageInfoPaginated { + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; +} + // ==================================================== // Arguments // ==================================================== @@ -1656,39 +1532,6 @@ export interface GetAllTimelineQueryArgs { status?: Maybe; } -export interface HostsSourceArgs { - id?: Maybe; - - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - sort: HostsSortField; - - filterQuery?: Maybe; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; -} -export interface HostOverviewSourceArgs { - id?: Maybe; - - hostName: string; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface HostFirstLastSeenSourceArgs { - id?: Maybe; - - hostName: string; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -2093,12 +1936,6 @@ export namespace SourceResolvers { configuration?: ConfigurationResolver; /** The status of the source */ status?: StatusResolver; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ - Hosts?: HostsResolver; - - HostOverview?: HostOverviewResolver; - - HostFirstLastSeen?: HostFirstLastSeenResolver; } export type IdResolver = Resolver< @@ -2116,57 +1953,6 @@ export namespace SourceResolvers { Parent, TContext >; - export type HostsResolver = Resolver< - R, - Parent, - TContext, - HostsArgs - >; - export interface HostsArgs { - id?: Maybe; - - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - sort: HostsSortField; - - filterQuery?: Maybe; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; - } - - export type HostOverviewResolver< - R = HostItem, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface HostOverviewArgs { - id?: Maybe; - - hostName: string; - - timerange: TimerangeInput; - - defaultIndex: string[]; - } - - export type HostFirstLastSeenResolver< - R = FirstLastSeenHost, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface HostFirstLastSeenArgs { - id?: Maybe; - - hostName: string; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; - } } /** A set of configuration options for a security data source */ export namespace SourceConfigurationResolvers { @@ -2247,410 +2033,14 @@ export namespace SourceStatusResolvers { defaultIndex: string[]; } - export type IndexFieldsResolver< - R = string[], - Parent = SourceStatus, - TContext = SiemContext - > = Resolver; - export interface IndexFieldsArgs { - defaultIndex: string[]; - } -} - -export namespace HostsDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver< - R = HostsEdges[], - Parent = HostsData, - TContext = SiemContext - > = Resolver; - export type TotalCountResolver = Resolver< - R, - Parent, - TContext - >; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = HostsData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = HostsData, - TContext = SiemContext - > = Resolver; -} - -export namespace HostsEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver = Resolver< - R, - Parent, - TContext - >; - export type CursorResolver< - R = CursorType, - Parent = HostsEdges, - TContext = SiemContext - > = Resolver; -} - -export namespace HostItemResolvers { - export interface Resolvers { - _id?: _IdResolver, TypeParent, TContext>; - - agent?: AgentResolver, TypeParent, TContext>; - - cloud?: CloudResolver, TypeParent, TContext>; - - endpoint?: EndpointResolver, TypeParent, TContext>; - - host?: HostResolver, TypeParent, TContext>; - - inspect?: InspectResolver, TypeParent, TContext>; - - lastSeen?: LastSeenResolver, TypeParent, TContext>; - } - - export type _IdResolver, Parent = HostItem, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type AgentResolver< - R = Maybe, - Parent = HostItem, - TContext = SiemContext - > = Resolver; - export type CloudResolver< - R = Maybe, - Parent = HostItem, - TContext = SiemContext - > = Resolver; - export type EndpointResolver< - R = Maybe, - Parent = HostItem, - TContext = SiemContext - > = Resolver; - export type HostResolver< - R = Maybe, - Parent = HostItem, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = HostItem, - TContext = SiemContext - > = Resolver; - export type LastSeenResolver< - R = Maybe, - Parent = HostItem, - TContext = SiemContext - > = Resolver; -} - -export namespace AgentFieldsResolvers { - export interface Resolvers { - id?: IdResolver, TypeParent, TContext>; - } - - export type IdResolver< - R = Maybe, - Parent = AgentFields, - TContext = SiemContext - > = Resolver; -} - -export namespace CloudFieldsResolvers { - export interface Resolvers { - instance?: InstanceResolver, TypeParent, TContext>; - - machine?: MachineResolver, TypeParent, TContext>; - - provider?: ProviderResolver)[]>, TypeParent, TContext>; - - region?: RegionResolver)[]>, TypeParent, TContext>; - } - - export type InstanceResolver< - R = Maybe, - Parent = CloudFields, - TContext = SiemContext - > = Resolver; - export type MachineResolver< - R = Maybe, - Parent = CloudFields, - TContext = SiemContext - > = Resolver; - export type ProviderResolver< - R = Maybe<(Maybe)[]>, - Parent = CloudFields, - TContext = SiemContext - > = Resolver; - export type RegionResolver< - R = Maybe<(Maybe)[]>, - Parent = CloudFields, - TContext = SiemContext - > = Resolver; -} - -export namespace CloudInstanceResolvers { - export interface Resolvers { - id?: IdResolver)[]>, TypeParent, TContext>; - } - - export type IdResolver< - R = Maybe<(Maybe)[]>, - Parent = CloudInstance, - TContext = SiemContext - > = Resolver; -} - -export namespace CloudMachineResolvers { - export interface Resolvers { - type?: TypeResolver)[]>, TypeParent, TContext>; - } - - export type TypeResolver< - R = Maybe<(Maybe)[]>, - Parent = CloudMachine, - TContext = SiemContext - > = Resolver; -} - -export namespace EndpointFieldsResolvers { - export interface Resolvers { - endpointPolicy?: EndpointPolicyResolver, TypeParent, TContext>; - - sensorVersion?: SensorVersionResolver, TypeParent, TContext>; - - policyStatus?: PolicyStatusResolver< - Maybe, - TypeParent, - TContext - >; - } - - export type EndpointPolicyResolver< - R = Maybe, - Parent = EndpointFields, - TContext = SiemContext - > = Resolver; - export type SensorVersionResolver< - R = Maybe, - Parent = EndpointFields, - TContext = SiemContext - > = Resolver; - export type PolicyStatusResolver< - R = Maybe, - Parent = EndpointFields, - TContext = SiemContext - > = Resolver; -} - -export namespace HostEcsFieldsResolvers { - export interface Resolvers { - architecture?: ArchitectureResolver, TypeParent, TContext>; - - id?: IdResolver, TypeParent, TContext>; - - ip?: IpResolver, TypeParent, TContext>; - - mac?: MacResolver, TypeParent, TContext>; - - name?: NameResolver, TypeParent, TContext>; - - os?: OsResolver, TypeParent, TContext>; - - type?: TypeResolver, TypeParent, TContext>; - } - - export type ArchitectureResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; - export type IdResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; - export type IpResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; - export type MacResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; - export type NameResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; - export type OsResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; - export type TypeResolver< - R = Maybe, - Parent = HostEcsFields, - TContext = SiemContext - > = Resolver; -} - -export namespace OsEcsFieldsResolvers { - export interface Resolvers { - platform?: PlatformResolver, TypeParent, TContext>; - - name?: NameResolver, TypeParent, TContext>; - - full?: FullResolver, TypeParent, TContext>; - - family?: FamilyResolver, TypeParent, TContext>; - - version?: VersionResolver, TypeParent, TContext>; - - kernel?: KernelResolver, TypeParent, TContext>; - } - - export type PlatformResolver< - R = Maybe, - Parent = OsEcsFields, - TContext = SiemContext - > = Resolver; - export type NameResolver< - R = Maybe, - Parent = OsEcsFields, - TContext = SiemContext - > = Resolver; - export type FullResolver< - R = Maybe, - Parent = OsEcsFields, - TContext = SiemContext - > = Resolver; - export type FamilyResolver< - R = Maybe, - Parent = OsEcsFields, - TContext = SiemContext - > = Resolver; - export type VersionResolver< - R = Maybe, - Parent = OsEcsFields, - TContext = SiemContext - > = Resolver; - export type KernelResolver< - R = Maybe, - Parent = OsEcsFields, - TContext = SiemContext - > = Resolver; -} - -export namespace InspectResolvers { - export interface Resolvers { - dsl?: DslResolver; - - response?: ResponseResolver; - } - - export type DslResolver = Resolver< - R, - Parent, - TContext - >; - export type ResponseResolver = Resolver< - R, - Parent, - TContext - >; -} - -export namespace CursorTypeResolvers { - export interface Resolvers { - value?: ValueResolver, TypeParent, TContext>; - - tiebreaker?: TiebreakerResolver, TypeParent, TContext>; - } - - export type ValueResolver< - R = Maybe, - Parent = CursorType, - TContext = SiemContext - > = Resolver; - export type TiebreakerResolver< - R = Maybe, - Parent = CursorType, - TContext = SiemContext - > = Resolver; -} - -export namespace PageInfoPaginatedResolvers { - export interface Resolvers { - activePage?: ActivePageResolver; - - fakeTotalCount?: FakeTotalCountResolver; - - showMorePagesIndicator?: ShowMorePagesIndicatorResolver; - } - - export type ActivePageResolver< - R = number, - Parent = PageInfoPaginated, - TContext = SiemContext - > = Resolver; - export type FakeTotalCountResolver< - R = number, - Parent = PageInfoPaginated, - TContext = SiemContext - > = Resolver; - export type ShowMorePagesIndicatorResolver< - R = boolean, - Parent = PageInfoPaginated, - TContext = SiemContext - > = Resolver; -} - -export namespace FirstLastSeenHostResolvers { - export interface Resolvers { - inspect?: InspectResolver, TypeParent, TContext>; - - firstSeen?: FirstSeenResolver, TypeParent, TContext>; - - lastSeen?: LastSeenResolver, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe, - Parent = FirstLastSeenHost, - TContext = SiemContext - > = Resolver; - export type FirstSeenResolver< - R = Maybe, - Parent = FirstLastSeenHost, - TContext = SiemContext - > = Resolver; - export type LastSeenResolver< - R = Maybe, - Parent = FirstLastSeenHost, + export type IndexFieldsResolver< + R = string[], + Parent = SourceStatus, TContext = SiemContext - > = Resolver; + > = Resolver; + export interface IndexFieldsArgs { + defaultIndex: string[]; + } } export namespace TimelineResultResolvers { @@ -3998,6 +3388,107 @@ export namespace AuditdEcsFieldsResolvers { > = Resolver; } +export namespace OsEcsFieldsResolvers { + export interface Resolvers { + platform?: PlatformResolver, TypeParent, TContext>; + + name?: NameResolver, TypeParent, TContext>; + + full?: FullResolver, TypeParent, TContext>; + + family?: FamilyResolver, TypeParent, TContext>; + + version?: VersionResolver, TypeParent, TContext>; + + kernel?: KernelResolver, TypeParent, TContext>; + } + + export type PlatformResolver< + R = Maybe, + Parent = OsEcsFields, + TContext = SiemContext + > = Resolver; + export type NameResolver< + R = Maybe, + Parent = OsEcsFields, + TContext = SiemContext + > = Resolver; + export type FullResolver< + R = Maybe, + Parent = OsEcsFields, + TContext = SiemContext + > = Resolver; + export type FamilyResolver< + R = Maybe, + Parent = OsEcsFields, + TContext = SiemContext + > = Resolver; + export type VersionResolver< + R = Maybe, + Parent = OsEcsFields, + TContext = SiemContext + > = Resolver; + export type KernelResolver< + R = Maybe, + Parent = OsEcsFields, + TContext = SiemContext + > = Resolver; +} + +export namespace HostEcsFieldsResolvers { + export interface Resolvers { + architecture?: ArchitectureResolver, TypeParent, TContext>; + + id?: IdResolver, TypeParent, TContext>; + + ip?: IpResolver, TypeParent, TContext>; + + mac?: MacResolver, TypeParent, TContext>; + + name?: NameResolver, TypeParent, TContext>; + + os?: OsResolver, TypeParent, TContext>; + + type?: TypeResolver, TypeParent, TContext>; + } + + export type ArchitectureResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; + export type IdResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; + export type IpResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; + export type MacResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; + export type NameResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; + export type OsResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; + export type TypeResolver< + R = Maybe, + Parent = HostEcsFields, + TContext = SiemContext + > = Resolver; +} + export namespace ThreadResolvers { export interface Resolvers { id?: IdResolver, TypeParent, TContext>; @@ -5887,103 +5378,21 @@ export namespace EcsEdgesResolvers { >; } -export namespace OsFieldsResolvers { - export interface Resolvers { - platform?: PlatformResolver, TypeParent, TContext>; - - name?: NameResolver, TypeParent, TContext>; - - full?: FullResolver, TypeParent, TContext>; - - family?: FamilyResolver, TypeParent, TContext>; - - version?: VersionResolver, TypeParent, TContext>; - - kernel?: KernelResolver, TypeParent, TContext>; - } - - export type PlatformResolver< - R = Maybe, - Parent = OsFields, - TContext = SiemContext - > = Resolver; - export type NameResolver, Parent = OsFields, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type FullResolver, Parent = OsFields, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type FamilyResolver< - R = Maybe, - Parent = OsFields, - TContext = SiemContext - > = Resolver; - export type VersionResolver< - R = Maybe, - Parent = OsFields, - TContext = SiemContext - > = Resolver; - export type KernelResolver< - R = Maybe, - Parent = OsFields, - TContext = SiemContext - > = Resolver; -} - -export namespace HostFieldsResolvers { - export interface Resolvers { - architecture?: ArchitectureResolver, TypeParent, TContext>; - - id?: IdResolver, TypeParent, TContext>; - - ip?: IpResolver)[]>, TypeParent, TContext>; - - mac?: MacResolver)[]>, TypeParent, TContext>; - - name?: NameResolver, TypeParent, TContext>; - - os?: OsResolver, TypeParent, TContext>; +export namespace CursorTypeResolvers { + export interface Resolvers { + value?: ValueResolver, TypeParent, TContext>; - type?: TypeResolver, TypeParent, TContext>; + tiebreaker?: TiebreakerResolver, TypeParent, TContext>; } - export type ArchitectureResolver< - R = Maybe, - Parent = HostFields, - TContext = SiemContext - > = Resolver; - export type IdResolver, Parent = HostFields, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type IpResolver< - R = Maybe<(Maybe)[]>, - Parent = HostFields, - TContext = SiemContext - > = Resolver; - export type MacResolver< - R = Maybe<(Maybe)[]>, - Parent = HostFields, - TContext = SiemContext - > = Resolver; - export type NameResolver< + export type ValueResolver< R = Maybe, - Parent = HostFields, - TContext = SiemContext - > = Resolver; - export type OsResolver< - R = Maybe, - Parent = HostFields, + Parent = CursorType, TContext = SiemContext > = Resolver; - export type TypeResolver< + export type TiebreakerResolver< R = Maybe, - Parent = HostFields, + Parent = CursorType, TContext = SiemContext > = Resolver; } @@ -6090,6 +5499,51 @@ export namespace PageInfoResolvers { > = Resolver; } +export namespace InspectResolvers { + export interface Resolvers { + dsl?: DslResolver; + + response?: ResponseResolver; + } + + export type DslResolver = Resolver< + R, + Parent, + TContext + >; + export type ResponseResolver = Resolver< + R, + Parent, + TContext + >; +} + +export namespace PageInfoPaginatedResolvers { + export interface Resolvers { + activePage?: ActivePageResolver; + + fakeTotalCount?: FakeTotalCountResolver; + + showMorePagesIndicator?: ShowMorePagesIndicatorResolver; + } + + export type ActivePageResolver< + R = number, + Parent = PageInfoPaginated, + TContext = SiemContext + > = Resolver; + export type FakeTotalCountResolver< + R = number, + Parent = PageInfoPaginated, + TContext = SiemContext + > = Resolver; + export type ShowMorePagesIndicatorResolver< + R = boolean, + Parent = PageInfoPaginated, + TContext = SiemContext + > = Resolver; +} + /** Directs the executor to skip this field or fragment when the `if` argument is true. */ export type SkipDirectiveResolver = DirectiveResolverFn< Result, @@ -6123,15 +5577,12 @@ export interface DeprecatedDirectiveArgs { reason?: string; } -export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig { - name: 'ToStringArray'; -} -export interface DateScalarConfig extends GraphQLScalarTypeConfig { - name: 'Date'; -} export interface ToAnyScalarConfig extends GraphQLScalarTypeConfig { name: 'ToAny'; } +export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig { + name: 'ToStringArray'; +} export interface ToStringArrayNoNullableScalarConfig extends GraphQLScalarTypeConfig { name: 'ToStringArrayNoNullable'; @@ -6145,6 +5596,9 @@ export interface ToNumberArrayScalarConfig extends GraphQLScalarTypeConfig { name: 'ToBooleanArray'; } +export interface DateScalarConfig extends GraphQLScalarTypeConfig { + name: 'Date'; +} export interface ToIFieldSubTypeNonNullableScalarConfig extends GraphQLScalarTypeConfig { name: 'ToIFieldSubTypeNonNullable'; @@ -6159,20 +5613,6 @@ export type IResolvers = { SourceConfiguration?: SourceConfigurationResolvers.Resolvers; SourceFields?: SourceFieldsResolvers.Resolvers; SourceStatus?: SourceStatusResolvers.Resolvers; - HostsData?: HostsDataResolvers.Resolvers; - HostsEdges?: HostsEdgesResolvers.Resolvers; - HostItem?: HostItemResolvers.Resolvers; - AgentFields?: AgentFieldsResolvers.Resolvers; - CloudFields?: CloudFieldsResolvers.Resolvers; - CloudInstance?: CloudInstanceResolvers.Resolvers; - CloudMachine?: CloudMachineResolvers.Resolvers; - EndpointFields?: EndpointFieldsResolvers.Resolvers; - HostEcsFields?: HostEcsFieldsResolvers.Resolvers; - OsEcsFields?: OsEcsFieldsResolvers.Resolvers; - Inspect?: InspectResolvers.Resolvers; - CursorType?: CursorTypeResolvers.Resolvers; - PageInfoPaginated?: PageInfoPaginatedResolvers.Resolvers; - FirstLastSeenHost?: FirstLastSeenHostResolvers.Resolvers; TimelineResult?: TimelineResultResolvers.Resolvers; ColumnHeaderResult?: ColumnHeaderResultResolvers.Resolvers; DataProviderResult?: DataProviderResultResolvers.Resolvers; @@ -6198,6 +5638,8 @@ export type IResolvers = { AgentEcsField?: AgentEcsFieldResolvers.Resolvers; AuditdData?: AuditdDataResolvers.Resolvers; AuditdEcsFields?: AuditdEcsFieldsResolvers.Resolvers; + OsEcsFields?: OsEcsFieldsResolvers.Resolvers; + HostEcsFields?: HostEcsFieldsResolvers.Resolvers; Thread?: ThreadResolvers.Resolvers; ProcessHashData?: ProcessHashDataResolvers.Resolvers; ProcessEcsFields?: ProcessEcsFieldsResolvers.Resolvers; @@ -6241,17 +5683,18 @@ export type IResolvers = { RuleEcsField?: RuleEcsFieldResolvers.Resolvers; Ecs?: EcsResolvers.Resolvers; EcsEdges?: EcsEdgesResolvers.Resolvers; - OsFields?: OsFieldsResolvers.Resolvers; - HostFields?: HostFieldsResolvers.Resolvers; + CursorType?: CursorTypeResolvers.Resolvers; IndexField?: IndexFieldResolvers.Resolvers; PageInfo?: PageInfoResolvers.Resolvers; - ToStringArray?: GraphQLScalarType; - Date?: GraphQLScalarType; + Inspect?: InspectResolvers.Resolvers; + PageInfoPaginated?: PageInfoPaginatedResolvers.Resolvers; ToAny?: GraphQLScalarType; + ToStringArray?: GraphQLScalarType; ToStringArrayNoNullable?: GraphQLScalarType; ToDateArray?: GraphQLScalarType; ToNumberArray?: GraphQLScalarType; ToBooleanArray?: GraphQLScalarType; + Date?: GraphQLScalarType; ToIFieldSubTypeNonNullable?: GraphQLScalarType; } & { [typeName: string]: never }; diff --git a/x-pack/plugins/security_solution/server/init_server.ts b/x-pack/plugins/security_solution/server/init_server.ts index 1744d9b75ec11..d2810bf71f8ae 100644 --- a/x-pack/plugins/security_solution/server/init_server.ts +++ b/x-pack/plugins/security_solution/server/init_server.ts @@ -9,7 +9,6 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { schemas } from './graphql'; import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; -import { createHostsResolvers } from './graphql/hosts'; import { createNoteResolvers } from './graphql/note'; import { createPinnedEventResolvers } from './graphql/pinned_event'; import { createScalarDateResolvers } from './graphql/scalar_date'; @@ -25,7 +24,6 @@ import { AppBackendLibs } from './lib/types'; export const initServer = (libs: AppBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ - createHostsResolvers(libs) as IResolvers, createNoteResolvers(libs) as IResolvers, createPinnedEventResolvers(libs) as IResolvers, createSourcesResolvers(libs) as IResolvers, diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index 5c83f70fdb10b..01318c87f8b3f 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -9,7 +9,6 @@ import { CoreSetup } from '../../../../../../src/core/server'; import { SetupPlugins } from '../../plugin'; import { KibanaBackendFrameworkAdapter } from '../framework/kibana_framework_adapter'; -import { ElasticsearchHostsAdapter, Hosts } from '../hosts'; import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; @@ -30,7 +29,6 @@ export function compose( const domainLibs: AppDomainLibs = { fields: new IndexFields(new ElasticsearchIndexFieldAdapter()), - hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), }; const libs: AppBackendLibs = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index c5cbbeb09ed6d..ef7236084508d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -27,12 +27,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('create_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index dd636d5a180d9..d6693dc1f7a0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -29,12 +29,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('create_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 0a265adf620ee..b0b4232651803 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -34,7 +34,7 @@ describe('import_rules_route', () => { let server: ReturnType; let request: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); @@ -42,7 +42,7 @@ describe('import_rules_route', () => { config = createMockConfig(); const hapiStream = buildHapiStream(ruleIdsToNdJsonString(['rule-1'])); request = getImportRulesRequest(hapiStream); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 88250fb920d6c..93fdf9c5f8194 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -24,12 +24,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.alertsClient.update.mockResolvedValue(getResult()); // update succeeds diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 1f21a11f22ef5..6e62f65f44858 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -26,12 +26,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 09ac156c375ee..41b31b04e3424 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -26,12 +26,12 @@ jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.update.mockResolvedValue(getResult()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index e5bea42bc49a1..c80d32e09ccab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -28,12 +28,12 @@ jest.mock('../../rules/update_rules_notifications'); describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let ml: ReturnType; + let ml: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - ml = mlServicesMock.create(); + ml = mlServicesMock.createSetupContract(); clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json index e3dcd03d9cd8b..bf53625cef750 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json index a1cde4af47028..43cb19f50d675 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json index 5be8ce5ae1ce1..29b5bc3f39cf1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json index 9205ad9a0a028..393591a241114 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json index af8e9640dbba7..e9ca199c4a791 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json index cb955134a9cef..a169582c2da92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json index b306dd0109568..b781a1fae1847 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json index 7519ef1be013c..f7a064961f039 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json index 47224626698c8..59cbd98e2d42b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json index ea3b0be99e70e..b3db96d6d121b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json index a9eccf31f2972..18b316a293da8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json index 13a9fc457fe42..861daa2d004c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json index 8d1254eaa0271..5f78a3517e931 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json index 4ef637205c008..4c060bb52f32f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json index 718d4728be675..78845ffc4c845 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json @@ -20,5 +20,5 @@ "Elastic Endgame" ], "type": "query", - "version": 4 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts index 4ed53e39fa5eb..813e800f34ce2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts @@ -53,3 +53,27 @@ export const type: SavedObjectsType = { namespaceType: 'single', mappings: ruleStatusSavedObjectMappings, }; + +export const ruleAssetSavedObjectType = 'security-rule'; + +export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + name: { + type: 'keyword', + }, + rule_id: { + type: 'keyword', + }, + version: { + type: 'long', + }, + }, +}; + +export const ruleAssetType: SavedObjectsType = { + name: ruleAssetSavedObjectType, + hidden: false, + namespaceType: 'agnostic', + mappings: ruleAssetSavedObjectMappings, +}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts deleted file mode 100644 index 04d52a044e390..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts +++ /dev/null @@ -1,252 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FirstLastSeenHost, HostItem, HostsData, HostsEdges } from '../../graphql/types'; -import { FrameworkAdapter, FrameworkRequest } from '../framework'; - -import { ElasticsearchHostsAdapter, formatHostEdgesData } from './elasticsearch_adapter'; -import { - mockEndpointMetadata, - mockGetHostOverviewOptions, - mockGetHostOverviewRequest, - mockGetHostOverviewResponse, - mockGetHostOverviewResult, - mockGetHostLastFirstSeenOptions, - mockGetHostLastFirstSeenRequest, - mockGetHostsOptions, - mockGetHostsRequest, - mockGetHostsResponse, - mockGetHostsResult, - mockGetHostLastFirstSeenResult, - mockGetHostLastFirstSeenResponse, - mockGetHostOverviewRequestDsl, - mockGetHostLastFirstSeenDsl, - mockGetHostsQueryDsl, -} from './mock'; -import { HostAggEsItem } from './types'; -import { EndpointAppContext } from '../../endpoint/types'; -import { mockLogger } from '../detection_engine/signals/__mocks__/es_results'; -import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; -import { - createMockEndpointAppContextServiceStartContract, - createMockPackageService, -} from '../../endpoint/mocks'; -import { PackageService } from '../../../../fleet/server/services'; -import { ElasticsearchAssetType } from '../../../../fleet/common/types/models'; -import { parseExperimentalConfigValue } from '../../../common/experimental_features'; - -jest.mock('./query.hosts.dsl', () => { - return { - buildHostsQuery: jest.fn(() => mockGetHostsQueryDsl), - }; -}); - -jest.mock('./query.detail_host.dsl', () => { - return { - buildHostOverviewQuery: jest.fn(() => mockGetHostOverviewRequestDsl), - }; -}); - -jest.mock('./query.last_first_seen_host.dsl', () => { - return { - buildLastFirstSeenHostQuery: jest.fn(() => mockGetHostLastFirstSeenDsl), - }; -}); -jest.mock('../../endpoint/routes/metadata/handlers', () => { - return { - getHostData: jest.fn(() => mockEndpointMetadata), - }; -}); - -describe('hosts elasticsearch_adapter', () => { - describe('#formatHostsData', () => { - const buckets: HostAggEsItem = { - key: 'zeek-london', - os: { - hits: { - total: { - value: 242338, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _index: 'auditbeat-8.0.0-2019.09.06-000022', - _id: 'dl0T_m0BHe9nqdOiF2A8', - _score: null, - _source: { - host: { - os: { - kernel: '5.0.0-1013-gcp', - name: 'Ubuntu', - family: 'debian', - version: '18.04.2 LTS (Bionic Beaver)', - platform: 'ubuntu', - }, - }, - }, - sort: [1571925726017], - }, - ], - }, - }, - }; - - test('it formats a host with a source of name correctly', () => { - const fields: readonly string[] = ['host.name']; - const data = formatHostEdgesData(fields, buckets); - const expected: HostsEdges = { - cursor: { tiebreaker: null, value: 'zeek-london' }, - node: { host: { name: 'zeek-london' }, _id: 'zeek-london' }, - }; - - expect(data).toEqual(expected); - }); - - test('it formats a host with a source of os correctly', () => { - const fields: readonly string[] = ['host.os.name']; - const data = formatHostEdgesData(fields, buckets); - const expected: HostsEdges = { - cursor: { tiebreaker: null, value: 'zeek-london' }, - node: { host: { os: { name: 'Ubuntu' } }, _id: 'zeek-london' }, - }; - - expect(data).toEqual(expected); - }); - - test('it formats a host with a source of version correctly', () => { - const fields: readonly string[] = ['host.os.version']; - const data = formatHostEdgesData(fields, buckets); - const expected: HostsEdges = { - cursor: { tiebreaker: null, value: 'zeek-london' }, - node: { host: { os: { version: '18.04.2 LTS (Bionic Beaver)' } }, _id: 'zeek-london' }, - }; - - expect(data).toEqual(expected); - }); - - test('it formats a host with a source of id correctly', () => { - const fields: readonly string[] = ['host.name']; - const data = formatHostEdgesData(fields, buckets); - const expected: HostsEdges = { - cursor: { tiebreaker: null, value: 'zeek-london' }, - node: { _id: 'zeek-london', host: { name: 'zeek-london' } }, - }; - - expect(data).toEqual(expected); - }); - - test('it formats a host with a source of name, lastBeat, os, and version correctly', () => { - const fields: readonly string[] = ['host.name', 'host.os.name', 'host.os.version']; - const data = formatHostEdgesData(fields, buckets); - const expected: HostsEdges = { - cursor: { tiebreaker: null, value: 'zeek-london' }, - node: { - _id: 'zeek-london', - host: { - name: 'zeek-london', - os: { name: 'Ubuntu', version: '18.04.2 LTS (Bionic Beaver)' }, - }, - }, - }; - - expect(data).toEqual(expected); - }); - - test('it formats a host without any data if fields are empty', () => { - const fields: readonly string[] = []; - const data = formatHostEdgesData(fields, buckets); - const expected: HostsEdges = { - cursor: { - tiebreaker: null, - value: '', - }, - node: {}, - }; - - expect(data).toEqual(expected); - }); - }); - - const endpointAppContextService = new EndpointAppContextService(); - const startContract = createMockEndpointAppContextServiceStartContract(); - const mockPackageService: jest.Mocked = createMockPackageService(); - mockPackageService.getInstalledEsAssetReferences.mockReturnValue( - Promise.resolve([ - { - id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', - type: ElasticsearchAssetType.transform, - }, - ]) - ); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - const endpointContext: EndpointAppContext = { - logFactory: mockLogger, - service: endpointAppContextService, - config: jest.fn(), - experimentalFeatures: parseExperimentalConfigValue([]), - }; - describe('#getHosts', () => { - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockGetHostsResponse); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); - - test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); - const data: HostsData = await EsHosts.getHosts( - mockGetHostsRequest as FrameworkRequest, - mockGetHostsOptions - ); - expect(data).toEqual(mockGetHostsResult); - }); - }); - - describe('#getHostOverview', () => { - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockGetHostOverviewResponse); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); - - test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); - const data: HostItem = await EsHosts.getHostOverview( - mockGetHostOverviewRequest as FrameworkRequest, - mockGetHostOverviewOptions - ); - expect(data).toEqual(mockGetHostOverviewResult); - }); - }); - - describe('#getHostLastFirstSeen', () => { - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockGetHostLastFirstSeenResponse); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); - - test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); - const data: FirstLastSeenHost = await EsHosts.getHostFirstLastSeen( - mockGetHostLastFirstSeenRequest as FrameworkRequest, - mockGetHostLastFirstSeenOptions - ); - expect(data).toEqual(mockGetHostLastFirstSeenResult); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts deleted file mode 100644 index 49066c099af38..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts +++ /dev/null @@ -1,236 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { set } from '@elastic/safer-lodash-set/fp'; -import { get, getOr, has, head } from 'lodash/fp'; - -import { - EndpointFields, - FirstLastSeenHost, - HostItem, - HostsData, - HostsEdges, -} from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { hostFieldsMap } from '../ecs_fields'; -import { FrameworkAdapter, FrameworkRequest } from '../framework'; -import { TermAggregation } from '../types'; -import { buildHostOverviewQuery } from './query.detail_host.dsl'; -import { buildHostsQuery } from './query.hosts.dsl'; -import { buildLastFirstSeenHostQuery } from './query.last_first_seen_host.dsl'; -import { - HostAggEsData, - HostAggEsItem, - HostBuckets, - HostEsData, - HostLastFirstSeenRequestOptions, - HostOverviewRequestOptions, - HostsAdapter, - HostsRequestOptions, - HostValue, -} from './types'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { EndpointAppContext } from '../../endpoint/types'; -import { getHostData } from '../../endpoint/routes/metadata/handlers'; - -export class ElasticsearchHostsAdapter implements HostsAdapter { - constructor( - private readonly framework: FrameworkAdapter, - private readonly endpointContext: EndpointAppContext - ) {} - - public async getHosts( - request: FrameworkRequest, - options: HostsRequestOptions - ): Promise { - const dsl = buildHostsQuery(options); - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.host_count.value', response); - const buckets: HostAggEsItem[] = getOr([], 'aggregations.host_data.buckets', response); - const hostsEdges = buckets.map((bucket) => formatHostEdgesData(options.fields, bucket)); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - - return { - inspect, - edges, - totalCount, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - }; - } - - public async getHostOverview( - request: FrameworkRequest, - options: HostOverviewRequestOptions - ): Promise { - const dsl = buildHostOverviewQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const aggregations: HostAggEsItem = get('aggregations', response) || {}; - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - const formattedHostItem = formatHostItem(options.fields, aggregations); - const ident = // endpoint-generated ID, NOT elastic-agent-id - formattedHostItem.agent && formattedHostItem.agent.id - ? Array.isArray(formattedHostItem.agent.id) - ? formattedHostItem.agent.id[0] - : formattedHostItem.agent.id - : null; - const endpoint: EndpointFields | null = await this.getHostEndpoint(request, ident); - return { inspect, _id: options.hostName, ...formattedHostItem, endpoint }; - } - - public async getHostEndpoint( - request: FrameworkRequest, - id: string | null - ): Promise { - const logger = this.endpointContext.logFactory.get('metadata'); - try { - const agentService = this.endpointContext.service.getAgentService(); - if (agentService === undefined) { - throw new Error('agentService not available'); - } - const metadataRequestContext = { - endpointAppContextService: this.endpointContext.service, - logger, - requestHandlerContext: request.context, - }; - const endpointData = - id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null - ? await getHostData(metadataRequestContext, id) - : null; - return endpointData != null && endpointData.metadata - ? { - endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, - policyStatus: endpointData.metadata.Endpoint.policy.applied.status, - sensorVersion: endpointData.metadata.agent.version, - } - : null; - } catch (err) { - logger.warn(JSON.stringify(err, null, 2)); - return null; - } - } - - public async getHostFirstLastSeen( - request: FrameworkRequest, - options: HostLastFirstSeenRequestOptions - ): Promise { - const dsl = buildLastFirstSeenHostQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const aggregations: HostAggEsItem = get('aggregations', response) || {}; - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - - return { - inspect, - firstSeen: get('firstSeen.value_as_string', aggregations), - lastSeen: get('lastSeen.value_as_string', aggregations), - }; - } -} - -export const formatHostEdgesData = (fields: readonly string[], bucket: HostAggEsItem): HostsEdges => - fields.reduce( - (flattenedFields, fieldName) => { - const hostId = get('key', bucket); - flattenedFields.node._id = hostId || null; - flattenedFields.cursor.value = hostId || ''; - const fieldValue = getHostFieldValue(fieldName, bucket); - if (fieldValue != null) { - return set(`node.${fieldName}`, fieldValue, flattenedFields); - } - return flattenedFields; - }, - { - node: {}, - cursor: { - value: '', - tiebreaker: null, - }, - } - ); - -const formatHostItem = (fields: readonly string[], bucket: HostAggEsItem): HostItem => - fields.reduce((flattenedFields, fieldName) => { - const fieldValue = getHostFieldValue(fieldName, bucket); - if (fieldValue != null) { - return set(fieldName, fieldValue, flattenedFields); - } - return flattenedFields; - }, {}); - -const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { - const aggField = hostFieldsMap[fieldName] - ? hostFieldsMap[fieldName].replace(/\./g, '_') - : fieldName.replace(/\./g, '_'); - if ( - [ - 'host.ip', - 'host.mac', - 'cloud.instance.id', - 'cloud.machine.type', - 'cloud.provider', - 'cloud.region', - ].includes(fieldName) && - has(aggField, bucket) - ) { - const data: HostBuckets = get(aggField, bucket); - return data.buckets.map((obj) => obj.key); - } else if (has(`${aggField}.buckets`, bucket)) { - return getFirstItem(get(`${aggField}`, bucket)); - } else if (has(aggField, bucket)) { - const valueObj: HostValue = get(aggField, bucket); - return valueObj.value_as_string; - } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { - switch (fieldName) { - case 'host.name': - return get('key', bucket) || null; - case 'host.os.name': - return get('os.hits.hits[0]._source.host.os.name', bucket) || null; - case 'host.os.version': - return get('os.hits.hits[0]._source.host.os.version', bucket) || null; - } - } - return null; -}; - -const getFirstItem = (data: HostBuckets): string | null => { - const firstItem = head(data.buckets); - if (firstItem == null) { - return null; - } - return firstItem.key; -}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts b/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts deleted file mode 100644 index 23d798f558403..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts +++ /dev/null @@ -1,62 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AggregationRequest } from '../types'; - -export const buildFieldsTermAggregation = (esFields: readonly string[]): AggregationRequest => - esFields.reduce( - (res, field) => ({ - ...res, - ...getTermsAggregationTypeFromField(field), - }), - {} - ); - -const getTermsAggregationTypeFromField = (field: string): AggregationRequest => { - if (field === 'host.ip') { - return { - host_ip: { - terms: { - script: { - source: "doc['host.ip']", - lang: 'painless', - }, - size: 10, - order: { - timestamp: 'desc', - }, - }, - aggs: { - timestamp: { - max: { - field: '@timestamp', - }, - }, - }, - }, - }; - } - - return { - [field.replace(/\./g, '_')]: { - terms: { - field, - size: 10, - order: { - timestamp: 'desc', - }, - }, - aggs: { - timestamp: { - max: { - field: '@timestamp', - }, - }, - }, - }, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/index.ts b/x-pack/plugins/security_solution/server/lib/hosts/index.ts deleted file mode 100644 index 07c71815ddbcb..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FirstLastSeenHost, HostItem, HostsData } from '../../graphql/types'; -import { FrameworkRequest } from '../framework'; - -import { - HostOverviewRequestOptions, - HostLastFirstSeenRequestOptions, - HostsAdapter, - HostsRequestOptions, -} from './types'; - -export * from './elasticsearch_adapter'; -export * from './types'; - -export class Hosts { - constructor(private readonly adapter: HostsAdapter) {} - - public async getHosts(req: FrameworkRequest, options: HostsRequestOptions): Promise { - return this.adapter.getHosts(req, options); - } - - public async getHostOverview( - req: FrameworkRequest, - options: HostOverviewRequestOptions - ): Promise { - return this.adapter.getHostOverview(req, options); - } - - public async getHostFirstLastSeen( - req: FrameworkRequest, - options: HostLastFirstSeenRequestOptions - ): Promise { - return this.adapter.getHostFirstLastSeen(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts deleted file mode 100644 index 431a26d81254c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ /dev/null @@ -1,648 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; -import { Direction, HostsFields } from '../../graphql/types'; -import { - HostOverviewRequestOptions, - HostLastFirstSeenRequestOptions, - HostsRequestOptions, -} from '.'; - -export const mockGetHostsOptions: HostsRequestOptions = { - defaultIndex: DEFAULT_INDEX_PATTERN, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: '2019-04-09T15:37:54.610Z', from: '2019-04-08T15:37:54.610Z' }, - sort: { field: HostsFields.lastSeen, direction: Direction.asc }, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 10, - querySize: 2, - }, - filterQuery: {}, - fields: [ - 'totalCount', - '_id', - 'host.id', - 'host.name', - 'host.os.name', - 'host.os.version', - 'edges.cursor.value', - 'pageInfo.activePage', - 'pageInfo.fakeTotalCount', - 'pageInfo.showMorePagesIndicator', - ], -}; - -export const mockGetHostsRequest = { - body: { - operationName: 'GetHostsTableQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: 1554737729201, to: 1554824129202 }, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 10, - querySize: 2, - }, - sort: { field: HostsFields.lastSeen, direction: Direction.asc }, - filterQuery: '', - }, - query: - 'query GetHostsTableQuery($sourceId: ID!, $timerange: TimerangeInput!, $pagination: PaginationInput!, $sort: HostsSortField!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n Hosts(timerange: $timerange, pagination: $pagination, sort: $sort, filterQuery: $filterQuery) {\n totalCount\n edges {\n node {\n _id\n host {\n id\n name\n os {\n name\n version\n __typename\n }\n __typename\n }\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor {\n value\n __typename\n }\n hasNextPage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockGetHostsResponse = { - took: 1695, - timed_out: false, - _shards: { - total: 59, - successful: 59, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 4018586, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - host_data: { - doc_count_error_upper_bound: -1, - sum_other_doc_count: 3082125, - buckets: [ - { - key: 'beats-ci-immutable-centos-7-1554823376629262884', - doc_count: 991, - lastSeen: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - host_os_version: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '7 (Core)', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - firstSeen: { - value: 1554823396740, - value_as_string: '2019-04-09T15:23:16.740Z', - }, - host_os_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'CentOS Linux', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - host_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'beats-ci-immutable-centos-7-1554823376629262884', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - host_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'f85edea1973c34f862c376cac4ebc777', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - }, - { - key: 'beats-ci-immutable-centos-7-1554823376629299914', - doc_count: 571, - lastSeen: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - host_os_version: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '7 (Core)', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - firstSeen: { - value: 1554823398628, - value_as_string: '2019-04-09T15:23:18.628Z', - }, - host_os_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'CentOS Linux', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - host_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'beats-ci-immutable-centos-7-1554823376629299914', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - host_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'f85edea1973c34f862c376cac4ebc777', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - }, - ], - }, - host_count: { - value: 1627, - }, - }, -}; - -export const mockGetHostsQueryDsl = { mockGetHostsQueryDsl: 'mockGetHostsQueryDsl' }; - -export const mockGetHostsResult = { - inspect: { - dsl: [JSON.stringify(mockGetHostsQueryDsl, null, 2)], - response: [JSON.stringify(mockGetHostsResponse, null, 2)], - }, - edges: [ - { - node: { - _id: 'beats-ci-immutable-centos-7-1554823376629262884', - host: { - id: 'f85edea1973c34f862c376cac4ebc777', - name: 'beats-ci-immutable-centos-7-1554823376629262884', - os: { - name: 'CentOS Linux', - version: '7 (Core)', - }, - }, - }, - cursor: { - value: 'beats-ci-immutable-centos-7-1554823376629262884', - tiebreaker: null, - }, - }, - { - node: { - _id: 'beats-ci-immutable-centos-7-1554823376629299914', - host: { - id: 'f85edea1973c34f862c376cac4ebc777', - name: 'beats-ci-immutable-centos-7-1554823376629299914', - os: { - name: 'CentOS Linux', - version: '7 (Core)', - }, - }, - }, - cursor: { - value: 'beats-ci-immutable-centos-7-1554823376629299914', - tiebreaker: null, - }, - }, - ], - totalCount: 1627, - pageInfo: { - activePage: 0, - fakeTotalCount: 10, - showMorePagesIndicator: true, - }, -}; - -export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: '2019-04-09T15:37:54.610Z', from: '2019-04-08T15:37:54.610Z' }, - defaultIndex: DEFAULT_INDEX_PATTERN, - fields: [ - '_id', - 'agent.id', - 'host.architecture', - 'host.id', - 'host.ip', - 'host.mac', - 'host.name', - 'host.os.family', - 'host.os.name', - 'host.os.platform', - 'host.os.version', - 'host.os.__typename', - 'host.type', - 'host.__typename', - 'cloud.instance.id', - 'cloud.instance.__typename', - 'cloud.machine.type', - 'cloud.machine.__typename', - 'cloud.provider', - 'cloud.region', - 'cloud.__typename', - '__typename', - ], - hostName: 'siem-es', -}; - -export const mockGetHostOverviewRequest = { - body: { - operationName: 'GetHostOverviewQuery', - variables: { sourceId: 'default', hostName: 'siem-es' }, - query: - 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n agent {\n id\n }\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockGetHostOverviewResponse = { - took: 2205, - timed_out: false, - _shards: { total: 59, successful: 59, skipped: 0, failed: 0 }, - hits: { total: { value: 611894, relation: 'eq' }, max_score: null, hits: [] }, - aggregations: { - host_mac: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - host_ip: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - cloud_region: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'us-east-1', - doc_count: 4308, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - cloud_provider: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'gce', - doc_count: 432808, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - cloud_instance_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '5412578377715150143', - doc_count: 432808, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - cloud_machine_type: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'n1-standard-1', - doc_count: 432808, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - host_os_version: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '9 (stretch)', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_architecture: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'x86_64', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_os_platform: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'debian', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_os_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'Debian GNU/Linux', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_os_family: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'debian', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'siem-es', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'b6d5264e4b9c8880ad1053841067a4a6', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - agent_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '9f48a9ab-749a-4ff0-b4e2-7e53910a985', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - }, -}; - -export const mockGetHostOverviewRequestDsl = { - mockGetHostOverviewRequestDsl: 'mockGetHostOverviewRequestDsl', -}; - -export const mockGetHostOverviewResult = { - inspect: { - dsl: [JSON.stringify(mockGetHostOverviewRequestDsl, null, 2)], - response: [JSON.stringify(mockGetHostOverviewResponse, null, 2)], - }, - _id: 'siem-es', - agent: { - id: '9f48a9ab-749a-4ff0-b4e2-7e53910a985', - }, - host: { - architecture: 'x86_64', - id: 'b6d5264e4b9c8880ad1053841067a4a6', - ip: [], - mac: [], - name: 'siem-es', - os: { - family: 'debian', - name: 'Debian GNU/Linux', - platform: 'debian', - version: '9 (stretch)', - }, - }, - cloud: { - instance: { - id: ['5412578377715150143'], - }, - machine: { - type: ['n1-standard-1'], - }, - provider: ['gce'], - region: ['us-east-1'], - }, - endpoint: { - endpointPolicy: 'demo', - policyStatus: 'success', - sensorVersion: '7.9.0-SNAPSHOT', - }, -}; - -export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = { - defaultIndex: DEFAULT_INDEX_PATTERN, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - hostName: 'siem-es', -}; - -export const mockGetHostLastFirstSeenRequest = { - body: { - operationName: 'GetHostLastFirstSeenQuery', - variables: { sourceId: 'default', hostName: 'siem-es' }, - query: - 'query GetHostLastFirstSeenQuery($sourceId: ID!, $hostName: String!) {\n source(id: $sourceId) {\n id\n HostLastFirstSeen(hostName: $hostName) {\n firstSeen\n lastSeen\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockGetHostLastFirstSeenResponse = { - took: 60, - timed_out: false, - _shards: { - total: 59, - successful: 59, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 612092, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - lastSeen: { - value: 1554826692178, - value_as_string: '2019-04-09T16:18:12.178Z', - }, - firstSeen: { - value: 1550806892826, - value_as_string: '2019-02-22T03:41:32.826Z', - }, - }, -}; - -export const mockGetHostLastFirstSeenDsl = { - mockGetHostLastFirstSeenDsl: 'mockGetHostLastFirstSeenDsl', -}; - -export const mockGetHostLastFirstSeenResult = { - inspect: { - dsl: [JSON.stringify(mockGetHostLastFirstSeenDsl, null, 2)], - response: [JSON.stringify(mockGetHostLastFirstSeenResponse, null, 2)], - }, - firstSeen: '2019-02-22T03:41:32.826Z', - lastSeen: '2019-04-09T16:18:12.178Z', -}; - -export const mockEndpointMetadata = { - metadata: { - '@timestamp': '2020-07-13T01:08:37.68896700Z', - Endpoint: { - policy: { - applied: { id: '3de86380-aa5a-11ea-b969-0bee1b260ab8', name: 'demo', status: 'success' }, - }, - status: 'enrolled', - }, - agent: { - build: { - original: - 'version: 7.9.0-SNAPSHOT, compiled: Thu Jul 09 07:56:12 2020, branch: 7.x, commit: 713a1071de475f15b3a1f0944d3602ed532597a5', - }, - id: 'c29e0de1-7476-480b-b242-38f0394bf6a1', - type: 'endpoint', - version: '7.9.0-SNAPSHOT', - }, - data_stream: { dataset: 'endpoint.metadata', namespace: 'default', type: 'metrics' }, - ecs: { version: '1.5.0' }, - elastic: { agent: { id: '' } }, - event: { - action: 'endpoint_metadata', - category: ['host'], - created: '2020-07-13T01:08:37.68896700Z', - dataset: 'endpoint.metadata', - id: 'Lkio+AHbZGSPFb7q++++++2E', - kind: 'metric', - module: 'endpoint', - sequence: 146, - type: ['info'], - }, - host: { - architecture: 'x86_64', - hostname: 'DESKTOP-4I1B23J', - id: 'a4148b63-1758-ab1f-a6d3-f95075cb1a9c', - ip: [ - '172.16.166.129', - 'fe80::c07e:eee9:3e8d:ea6d', - '169.254.205.96', - 'fe80::1027:b13d:a4a7:cd60', - '127.0.0.1', - '::1', - ], - mac: ['00:0c:29:89:ff:73', '3c:22:fb:3c:93:4c'], - name: 'DESKTOP-4I1B23J', - os: { - Ext: { variant: 'Windows 10 Pro' }, - family: 'windows', - full: 'Windows 10 Pro 2004 (10.0.19041.329)', - kernel: '2004 (10.0.19041.329)', - name: 'Windows', - platform: 'windows', - version: '2004 (10.0.19041.329)', - }, - }, - message: 'Endpoint metadata', - }, - host_status: 'error', -}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts deleted file mode 100644 index 4dd5a86e46bf6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { reduceFields } from '../../utils/build_query/reduce_fields'; -import { cloudFieldsMap, hostFieldsMap, agentFieldsMap } from '../ecs_fields'; - -import { buildFieldsTermAggregation } from './helpers'; -import { HostOverviewRequestOptions } from './types'; - -export const buildHostOverviewQuery = ({ - fields, - hostName, - defaultIndex, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: HostOverviewRequestOptions) => { - const esFields = reduceFields(fields, { ...hostFieldsMap, ...cloudFieldsMap, ...agentFieldsMap }); - - const filter = [ - { term: { 'host.name': hostName } }, - { - range: { - [timestamp]: { - format: 'strict_date_optional_time', - gte: from, - lte: to, - }, - }, - }, - ]; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - track_total_hits: false, - body: { - aggregations: { - ...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))), - }, - query: { bool: { filter } }, - size: 0, - }, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts deleted file mode 100644 index 16c53aa6a85eb..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts +++ /dev/null @@ -1,93 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash/fp'; - -import { assertUnreachable } from '../../../common/utility_types'; -import { Direction, HostsFields, HostsSortField } from '../../graphql/types'; -import { createQueryFilterClauses } from '../../utils/build_query'; - -import { HostsRequestOptions } from '.'; - -export const buildHostsQuery = ({ - defaultIndex, - docValueFields, - fields, - filterQuery, - pagination: { querySize }, - sort, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: HostsRequestOptions) => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ]; - - const agg = { host_count: { cardinality: { field: 'host.name' } } }; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - track_total_hits: false, - body: { - ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), - aggregations: { - ...agg, - host_data: { - terms: { size: querySize, field: 'host.name', order: getQueryOrder(sort) }, - aggs: { - lastSeen: { max: { field: '@timestamp' } }, - os: { - top_hits: { - size: 1, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - _source: { - includes: ['host.os.*'], - }, - }, - }, - }, - }, - }, - query: { bool: { filter } }, - size: 0, - }, - }; - - return dslQuery; -}; - -type QueryOrder = { lastSeen: Direction } | { _key: Direction }; - -const getQueryOrder = (sort: HostsSortField): QueryOrder => { - switch (sort.field) { - case HostsFields.lastSeen: - return { lastSeen: sort.direction }; - case HostsFields.hostName: - return { _key: sort.direction }; - default: - return assertUnreachable(sort.field); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts deleted file mode 100644 index a047be8ed2674..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash/fp'; -import { HostLastFirstSeenRequestOptions } from './types'; - -export const buildLastFirstSeenHostQuery = ({ - hostName, - defaultIndex, - docValueFields, -}: HostLastFirstSeenRequestOptions) => { - const filter = [{ term: { 'host.name': hostName } }]; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - track_total_hits: false, - body: { - ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), - aggregations: { - firstSeen: { min: { field: '@timestamp' } }, - lastSeen: { max: { field: '@timestamp' } }, - }, - query: { bool: { filter } }, - size: 0, - }, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/types.ts b/x-pack/plugins/security_solution/server/lib/hosts/types.ts deleted file mode 100644 index d18e42f606be7..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/hosts/types.ts +++ /dev/null @@ -1,129 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - FirstLastSeenHost, - HostEcsFields, - HostItem, - HostsData, - HostsSortField, - Maybe, - OsEcsFields, - SourceConfiguration, - TimerangeInput, - DocValueFieldsInput, -} from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; -import { Hit, Hits, SearchHit } from '../types'; -import { TotalValue } from '../../../common/detection_engine/types'; - -export interface HostsAdapter { - getHosts(req: FrameworkRequest, options: HostsRequestOptions): Promise; - getHostOverview(req: FrameworkRequest, options: HostOverviewRequestOptions): Promise; - getHostFirstLastSeen( - req: FrameworkRequest, - options: HostLastFirstSeenRequestOptions - ): Promise; -} - -type StringOrNumber = string | number; - -export interface HostHit extends Hit { - _source: { - '@timestamp'?: string; - host: HostEcsFields; - }; - cursor?: string; - firstSeen?: string; - sort?: StringOrNumber[]; -} - -export type HostHits = Hits; - -export interface HostsRequestOptions extends RequestOptionsPaginated { - sort: HostsSortField; - defaultIndex: string[]; -} - -export interface HostLastFirstSeenRequestOptions { - hostName: string; - sourceConfiguration: SourceConfiguration; - defaultIndex: string[]; - docValueFields?: DocValueFieldsInput[]; -} - -export interface HostOverviewRequestOptions extends HostLastFirstSeenRequestOptions { - fields: string[]; - timerange: TimerangeInput; - defaultIndex: string[]; -} - -export interface HostValue { - value: number; - value_as_string: string; -} - -export interface HostBucketItem { - key: string; - doc_count: number; - timestamp: HostValue; -} - -export interface HostBuckets { - buckets: HostBucketItem[]; -} - -export interface HostOsHitsItem { - hits: { - total: TotalValue | number; - max_score: number | null; - hits: Array<{ - _source: { host: { os: Maybe } }; - sort?: [number]; - _index?: string; - _type?: string; - _id?: string; - _score?: number | null; - }>; - }; -} - -export interface HostAggEsItem { - cloud_instance_id?: HostBuckets; - cloud_machine_type?: HostBuckets; - cloud_provider?: HostBuckets; - cloud_region?: HostBuckets; - firstSeen?: HostValue; - host_architecture?: HostBuckets; - host_id?: HostBuckets; - host_ip?: HostBuckets; - host_mac?: HostBuckets; - host_name?: HostBuckets; - host_os_name?: HostBuckets; - host_os_version?: HostBuckets; - host_type?: HostBuckets; - key?: string; - lastSeen?: HostValue; - os?: HostOsHitsItem; -} - -export interface HostEsData extends SearchHit { - sort: string[]; - aggregations: { - host_count: { - value: number; - }; - host_data: { - buckets: HostAggEsItem[]; - }; - }; -} - -export interface HostAggEsData extends SearchHit { - sort: string[]; - aggregations: HostAggEsItem; -} diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts index b41ba543675ec..d87c53ecfba71 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.test.ts @@ -16,7 +16,7 @@ jest.mock('../../../common/machine_learning/has_ml_admin_permissions'); describe('isMlAdmin', () => { it('returns true if hasMlAdminPermissions is true', async () => { - const mockMl = mlServicesMock.create(); + const mockMl = mlServicesMock.createSetupContract(); const request = httpServerMock.createKibanaRequest(); const savedObjectsClient = savedObjectsClientMock.create(); (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); @@ -25,7 +25,7 @@ describe('isMlAdmin', () => { }); it('returns false if hasMlAdminPermissions is false', async () => { - const mockMl = mlServicesMock.create(); + const mockMl = mlServicesMock.createSetupContract(); const request = httpServerMock.createKibanaRequest(); const savedObjectsClient = savedObjectsClientMock.create(); (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); @@ -56,13 +56,13 @@ describe('hasMlLicense', () => { describe('mlAuthz', () => { let licenseMock: ReturnType; - let mlMock: ReturnType; + let mlMock: ReturnType; let request: KibanaRequest; let savedObjectsClient: SavedObjectsClientContract; beforeEach(() => { licenseMock = licensingMock.createLicenseMock(); - mlMock = mlServicesMock.create(); + mlMock = mlServicesMock.createSetupContract(); request = httpServerMock.createKibanaRequest(); savedObjectsClient = savedObjectsClientMock.create(); }); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts index 5d1b090e98a79..a121a682d2892 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts @@ -5,25 +5,9 @@ * 2.0. */ -import { MlPluginSetup } from '../../../../ml/server'; -import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { mlPluginServerMock } from '../../../../ml/server/mocks'; -const createMockClient = () => elasticsearchServiceMock.createLegacyClusterClient(); -const createMockMlSystemProvider = () => - jest.fn(() => ({ - mlCapabilities: jest.fn(), - })); - -export const mlServicesMock = { - create: () => - (({ - modulesProvider: jest.fn(), - jobServiceProvider: jest.fn(), - anomalyDetectorsProvider: jest.fn(), - mlSystemProvider: createMockMlSystemProvider(), - mlClient: createMockClient(), - } as unknown) as jest.Mocked), -}; +export const mlServicesMock = mlPluginServerMock; const mockValidateRuleType = jest.fn().mockResolvedValue({ valid: true, message: undefined }); const createBuildMlAuthzMock = () => diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 8b2c1126e929f..f1c7a275e162c 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -10,7 +10,6 @@ export { ConfigType as Configuration } from '../config'; import type { SecuritySolutionRequestHandlerContext } from '../types'; import { FrameworkAdapter, FrameworkRequest } from './framework'; -import { Hosts } from './hosts'; import { IndexFields } from './index_fields'; import { SourceStatus } from './source_status'; import { Sources } from './sources'; @@ -19,11 +18,8 @@ import { PinnedEvent } from './timeline/saved_object/pinned_events'; import { Timeline } from './timeline/saved_object/timelines'; import { TotalValue, BaseHit, Explanation } from '../../common/detection_engine/types'; -export * from './hosts'; - export interface AppDomainLibs { fields: IndexFields; - hosts: Hosts; } export interface AppBackendLibs extends AppDomainLibs { @@ -142,54 +138,9 @@ export interface Hits { hits: U[]; }; } -export type SortRequestDirection = 'asc' | 'desc'; - -interface SortRequestField { - [field: string]: SortRequestDirection; -} - -export type SortRequest = SortRequestField[]; export interface MSearchHeader { index: string[] | string; allowNoIndices?: boolean; ignoreUnavailable?: boolean; } - -export interface AggregationRequest { - [aggField: string]: { - terms?: { - field?: string; - missing?: string; - size?: number; - script?: { - source: string; - lang: string; - }; - order?: { - [aggSortField: string]: SortRequestDirection; - }; - }; - max?: { - field: string; - }; - aggs?: { - [aggSortField: string]: { - [aggType: string]: { - field: string; - }; - }; - }; - top_hits?: { - size?: number; - sort?: Array<{ - [aggSortField: string]: { - order: SortRequestDirection; - }; - }>; - _source: { - includes: string[]; - }; - }; - }; -} diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index d483bd25266af..42abb3dab2ac4 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -8,7 +8,10 @@ import { CoreSetup } from '../../../../src/core/server'; import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings'; -import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings'; +import { + type as ruleStatusType, + ruleAssetType, +} from './lib/detection_engine/rules/saved_object_mappings'; import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; import { @@ -21,6 +24,7 @@ const types = [ pinnedEventType, ruleActionsType, ruleStatusType, + ruleAssetType, timelineType, exceptionsArtifactType, manifestType, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 8b2397fd7fab0..3f4eb5721164b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -8,9 +8,13 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; -import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; +import { + HostAggEsItem, + HostBuckets, + HostsEdges, + HostValue, +} from '../../../../../../common/search_strategy/security_solution/hosts'; -import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const HOSTS_FIELDS: readonly string[] = [ diff --git a/x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helper.test.ts similarity index 85% rename from x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helper.test.ts index 90c4f195fcf08..6dd2dc3834ae8 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helper.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AggregationRequest } from '../types'; - +import { Direction } from '../../../../../../common/search_strategy/common'; +import { AggregationRequest } from '../../../../../../common/search_strategy/security_solution/hosts'; import { buildFieldsTermAggregation } from './helpers'; describe('#buildFieldsTermAggregation', () => { @@ -25,7 +25,7 @@ describe('#buildFieldsTermAggregation', () => { field: 'host.architecture', size: 10, order: { - timestamp: 'desc', + timestamp: Direction.desc, }, }, aggs: { @@ -41,7 +41,7 @@ describe('#buildFieldsTermAggregation', () => { field: 'host.id', size: 10, order: { - timestamp: 'desc', + timestamp: Direction.desc, }, }, aggs: { @@ -60,7 +60,7 @@ describe('#buildFieldsTermAggregation', () => { }, size: 10, order: { - timestamp: 'desc', + timestamp: Direction.desc, }, }, aggs: { @@ -76,7 +76,7 @@ describe('#buildFieldsTermAggregation', () => { field: 'host.name', size: 10, order: { - timestamp: 'desc', + timestamp: Direction.desc, }, }, aggs: { @@ -92,7 +92,7 @@ describe('#buildFieldsTermAggregation', () => { field: 'host.os.family', size: 10, order: { - timestamp: 'desc', + timestamp: Direction.desc, }, }, aggs: { @@ -108,7 +108,7 @@ describe('#buildFieldsTermAggregation', () => { field: 'host.os.name', size: 10, order: { - timestamp: 'desc', + timestamp: Direction.desc, }, }, aggs: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 2b35517d693d5..d36af61957690 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -8,11 +8,16 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; -import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; +import { Direction } from '../../../../../../common/search_strategy/common'; +import { + AggregationRequest, + HostAggEsItem, + HostBuckets, + HostItem, + HostValue, +} from '../../../../../../common/search_strategy/security_solution/hosts'; import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; -import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; - export const HOST_FIELDS = [ '_id', 'host.architecture', @@ -35,6 +40,60 @@ export const HOST_FIELDS = [ 'endpoint.sensorVersion', ]; +export const buildFieldsTermAggregation = (esFields: readonly string[]): AggregationRequest => + esFields.reduce( + (res, field) => ({ + ...res, + ...getTermsAggregationTypeFromField(field), + }), + {} + ); + +const getTermsAggregationTypeFromField = (field: string): AggregationRequest => { + if (field === 'host.ip') { + return { + host_ip: { + terms: { + script: { + source: "doc['host.ip']", + lang: 'painless', + }, + size: 10, + order: { + timestamp: Direction.desc, + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }; + } + + return { + [field.replace(/\./g, '_')]: { + terms: { + field, + size: 10, + order: { + timestamp: Direction.desc, + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }; +}; + export const formatHostItem = (bucket: HostAggEsItem): HostItem => HOST_FIELDS.reduce((flattenedFields, fieldName) => { const fieldValue = getHostFieldValue(fieldName, bucket); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts index f340e4d905666..fb8296d6593b0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts @@ -8,9 +8,8 @@ import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; import { cloudFieldsMap, hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostDetailsRequestOptions } from '../../../../../../common/search_strategy/security_solution'; -import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers'; import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; -import { HOST_FIELDS } from './helpers'; +import { HOST_FIELDS, buildFieldsTermAggregation } from './helpers'; export const buildHostDetailsQuery = ({ hostName, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts index a6d5dcdf022b5..b492bf57f94a6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts @@ -6,6 +6,7 @@ */ import { + Direction, HostFirstLastSeenRequestOptions, HostsQueries, } from '../../../../../../../common/search_strategy'; @@ -23,7 +24,7 @@ export const mockOptions: HostFirstLastSeenRequestOptions = { docValueFields: [], factoryQueryType: HostsQueries.firstOrLastSeen, hostName: 'siem-kibana', - order: 'asc', + order: Direction.asc, }; export const mockSearchStrategyFirstSeenResponse = { @@ -141,7 +142,7 @@ export const formattedSearchStrategyFirstResponse = { sort: [ { '@timestamp': { - order: 'asc', + order: Direction.asc, }, }, ], @@ -206,7 +207,7 @@ export const formattedSearchStrategyLastResponse = { sort: [ { '@timestamp': { - order: 'desc', + order: Direction.desc, }, }, ], @@ -237,6 +238,6 @@ export const expectedDsl = { _source: ['@timestamp'], query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, size: 1, - sort: [{ '@timestamp': { order: 'asc' } }], + sort: [{ '@timestamp': { order: Direction.asc } }], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts index d0405d829b83d..2c1100fed0f9e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.test.ts @@ -14,7 +14,10 @@ import { formattedSearchStrategyLastResponse, formattedSearchStrategyFirstResponse, } from './__mocks__'; -import { HostFirstLastSeenRequestOptions } from '../../../../../../common/search_strategy'; +import { + Direction, + HostFirstLastSeenRequestOptions, +} from '../../../../../../common/search_strategy'; describe('firstLastSeenHost search strategy', () => { describe('first seen search strategy', () => { @@ -51,7 +54,7 @@ describe('firstLastSeenHost search strategy', () => { describe('buildDsl', () => { test('should build dsl query', () => { - const options: HostFirstLastSeenRequestOptions = { ...mockOptions, order: 'desc' }; + const options: HostFirstLastSeenRequestOptions = { ...mockOptions, order: Direction.desc }; firstOrLastSeenHost.buildDsl(options); expect(buildFirstLastSeenHostQuery).toHaveBeenCalledWith(options); }); @@ -60,7 +63,7 @@ describe('firstLastSeenHost search strategy', () => { describe('parse', () => { test('should parse data correctly', async () => { const result = await firstOrLastSeenHost.parse( - { ...mockOptions, order: 'desc' }, + { ...mockOptions, order: Direction.desc }, mockSearchStrategyLastSeenResponse ); expect(result).toMatchObject(formattedSearchStrategyLastResponse); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index b53f90f40f621..64a33068ad686 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -21,12 +21,12 @@ import { fetchDetectionsUsage, fetchDetectionsMetrics } from './index'; describe('Detections Usage and Metrics', () => { let esClientMock: jest.Mocked; let savedObjectsClientMock: jest.Mocked; - let mlMock: ReturnType; + let mlMock: ReturnType; describe('fetchDetectionsUsage()', () => { beforeEach(() => { esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; - mlMock = mlServicesMock.create(); + mlMock = mlServicesMock.createSetupContract(); }); it('returns zeroed counts if both calls are empty', async () => { @@ -108,7 +108,7 @@ describe('Detections Usage and Metrics', () => { describe('fetchDetectionsMetrics()', () => { beforeEach(() => { - mlMock = mlServicesMock.create(); + mlMock = mlServicesMock.createSetupContract(); }); it('returns an empty array if there is no data', async () => { diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts index 5bc970a1143a4..9b82c1d5b6364 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -24,13 +24,28 @@ const initTestBed = registerTestBed( ); const setupActions = (testBed: TestBed) => { - const { find, component, form } = testBed; + const { find, component, form, exists } = testBed; return { findDataStreamCallout() { return find('dataStreamWarningCallOut'); }, + canGoToADifferentStep() { + const canGoNext = find('restoreSnapshotsForm.nextButton').props().disabled !== true; + const canGoPrevious = exists('restoreSnapshotsForm.backButton') + ? find('restoreSnapshotsForm.nextButton').props().disabled !== true + : true; + return canGoNext && canGoPrevious; + }, + + toggleModifyIndexSettings() { + act(() => { + form.toggleEuiSwitch('modifyIndexSettingsSwitch'); + }); + component.update(); + }, + toggleGlobalState() { act(() => { form.toggleEuiSwitch('includeGlobalStateSwitch'); @@ -85,4 +100,7 @@ export type RestoreSnapshotFormTestSubject = | 'nextButton' | 'restoreButton' | 'systemIndicesInfoCallOut' - | 'dataStreamWarningCallOut'; + | 'dataStreamWarningCallOut' + | 'restoreSnapshotsForm.backButton' + | 'restoreSnapshotsForm.nextButton' + | 'modifyIndexSettingsSwitch'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts index 9f12415b70a9f..2d8c734af3605 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -22,6 +22,26 @@ describe('', () => { server.restore(); }); + describe('wizard navigation', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + it('does not allow navigation when the step is invalid', async () => { + const { actions } = testBed; + actions.goToStep(2); + expect(actions.canGoToADifferentStep()).toBe(true); + actions.toggleModifyIndexSettings(); + expect(actions.canGoToADifferentStep()).toBe(false); + }); + }); + describe('with data streams', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx index 82ace79f49f5d..a288484421813 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx @@ -104,7 +104,7 @@ export const RestoreSnapshotForm: React.FunctionComponent = ({ updateCurrentStep={updateCurrentStep} /> - + = ({ {currentStep > 1 ? ( - onBack()}> + onBack()} + disabled={!validation.isValid} + data-test-subj="backButton" + > = ( { * commented out due to hooks being called regardless of skip * https://github.com/facebook/jest/issues/8379 - beforeEach(async () => { + beforeEach(async () => { root = kbnTestServer.createRoot(); }); - afterEach(async () => await root.shutdown()); + afterEach(async () => await root.shutdown()); - */ + */ - function initKbnServer(router: IRouter, basePath: IBasePath, routes: 'legacy' | 'new-platform') { - const kbnServer = kbnTestServer.getKbnServer(root); - - if (routes === 'legacy') { - kbnServer.server.route([ - { - method: 'GET', - path: '/foo', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/app/kibana', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/app/app-1', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/app/app-2', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/api/test/foo', - handler: (req: Legacy.Request) => { - return { path: req.path, basePath: basePath.get(req) }; - }, - }, - { - method: 'GET', - path: '/some/path/s/foo/bar', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - ]); - } - - if (routes === 'new-platform') { - router.get({ path: '/api/np_test/foo', validate: false }, (context, req, h) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - }); - } + function initKbnServer(router: IRouter, basePath: IBasePath) { + router.get({ path: '/api/np_test/foo', validate: false }, (context, req, h) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + }); } async function request( @@ -205,12 +153,10 @@ describe.skip('onPostAuthInterceptor', () => { const router = http.createRouter('/'); - initKbnServer(router, http.basePath, 'new-platform'); + initKbnServer(router, http.basePath); await root.start(); - initKbnServer(router, http.basePath, 'legacy'); - const response = await kbnTestServer.request.get(root, path); return { @@ -219,58 +165,6 @@ describe.skip('onPostAuthInterceptor', () => { }; } - describe('requests proxied to the legacy platform', () => { - it('redirects to the space selector screen when accessing an app within a non-existent space', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - }, - ]; - - const { response } = await request('/s/not-found/app/kibana', spaces); - - expect(response.status).toEqual(302); - expect(response.header.location).toEqual(`/spaces/space_selector`); - }); - - it('when accessing the kibana app it always allows the request to continue', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - disabledFeatures: ['feature-1', 'feature-2', 'feature-4', 'feature-5'], - }, - }, - ]; - - const { response } = await request('/s/a-space/app/kibana', spaces); - - expect(response.status).toEqual(200); - }); - - it('allows the request to continue when accessing an API endpoint within a non-existent space', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - }, - ]; - - const { response } = await request('/s/not-found/api/test/foo', spaces); - - expect(response.status).toEqual(200); - }); - }); - describe('requests handled completely in the new platform', () => { it('redirects to the space selector screen when accessing an app within a non-existent space', async () => { const spaces = [ diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 27164109de74d..4bb21500f7bfc 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -6,7 +6,6 @@ */ import { schema } from '@kbn/config-schema'; -import type { Legacy } from 'kibana'; import type { CoreSetup, IBasePath, @@ -28,90 +27,57 @@ describe.skip('onRequestInterceptor', () => { * commented out due to hooks being called regardless of skip * https://github.com/facebook/jest/issues/8379 - beforeEach(async () => { + beforeEach(async () => { root = kbnTestServer.createRoot(); }, 30000); - afterEach(async () => await root.shutdown()); + afterEach(async () => await root.shutdown()); - */ + */ - function initKbnServer(router: IRouter, basePath: IBasePath, routes: 'legacy' | 'new-platform') { - const kbnServer = kbnTestServer.getKbnServer(root); + function initKbnServer(router: IRouter, basePath: IBasePath) { + router.get( + { path: '/np_foo', validate: false }, + (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + } + ); - if (routes === 'legacy') { - kbnServer.server.route([ - { - method: 'GET', - path: '/foo', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/some/path/s/foo/bar', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/i/love/spaces', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ - path: req.path, - basePath: basePath.get(req), - query: req.query, - }); - }, - }, - ]); - } - - if (routes === 'new-platform') { - router.get( - { path: '/np_foo', validate: false }, - (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - } - ); - - router.get( - { path: '/some/path/s/np_foo/bar', validate: false }, - (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - } - ); - - router.get( - { - path: '/i/love/np_spaces', - validate: { - query: schema.object({ - queryParam: schema.string({ - defaultValue: 'oh noes, this was not set on the request correctly', - }), + router.get( + { path: '/some/path/s/np_foo/bar', validate: false }, + (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + } + ); + + router.get( + { + path: '/i/love/np_spaces', + validate: { + query: schema.object({ + queryParam: schema.string({ + defaultValue: 'oh noes, this was not set on the request correctly', }), - }, + }), }, - (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { - return h.ok({ - body: { - path: req.url.pathname, - basePath: basePath.get(req), - query: req.query, - }, - }); - } - ); - } + }, + (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { + return h.ok({ + body: { + path: req.url.pathname, + basePath: basePath.get(req), + query: req.query, + }, + }); + } + ); } interface SetupOpts { basePath: string; routes: 'legacy' | 'new-platform'; } + async function setup(opts: SetupOpts = { basePath: '/', routes: 'legacy' }) { const { http, elasticsearch } = await root.setup(); // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check @@ -123,69 +89,15 @@ describe.skip('onRequestInterceptor', () => { const router = http.createRouter('/'); - initKbnServer(router, http.basePath, 'new-platform'); + initKbnServer(router, http.basePath); await root.start(); - initKbnServer(router, http.basePath, 'legacy'); - return { http, }; } - describe('requests proxied to the legacy platform', () => { - it('handles paths without a space identifier', async () => { - await setup(); - - const path = '/foo'; - - await kbnTestServer.request.get(root, path).expect(200, { - path, - basePath: '', // no base path set for route within the default space - }); - }, 30000); - - it('strips the Space URL Context from the request', async () => { - await setup(); - - const path = '/s/foo-space/foo'; - - const resp = await kbnTestServer.request.get(root, path); - - expect(resp.status).toEqual(200); - expect(resp.body).toEqual({ - path: '/foo', - basePath: '/s/foo-space', - }); - }, 30000); - - it('ignores space identifiers in the middle of the path', async () => { - await setup(); - - const path = '/some/path/s/foo/bar'; - - await kbnTestServer.request.get(root, path).expect(200, { - path: '/some/path/s/foo/bar', - basePath: '', // no base path set for route within the default space - }); - }, 30000); - - it('strips the Space URL Context from the request, maintaining the rest of the path', async () => { - await setup(); - - const path = '/s/foo/i/love/spaces?queryParam=queryValue'; - - await kbnTestServer.request.get(root, path).expect(200, { - path: '/i/love/spaces', - basePath: '/s/foo', - query: { - queryParam: 'queryValue', - }, - }); - }, 30000); - }); - describe('requests handled completely in the new platform', () => { it('handles paths without a space identifier', async () => { await setup({ basePath: '/', routes: 'new-platform' }); diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 4c67cbe8912bd..95fbecaa90936 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index cc86411cd9422..0d7ed390f20c7 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -10,7 +10,7 @@ import { IUiSettingsClient, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { interval } from 'rxjs'; import { - AnnotationDomainTypes, + AnnotationDomainType, Axis, Chart, LineAnnotation, @@ -303,7 +303,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ ); diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 9f91398cc7d24..e168ec21438c0 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -15,7 +15,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; export const BUILT_IN_ALERTS_FEATURE = { id: STACK_ALERTS_FEATURE_ID, name: i18n.translate('xpack.stackAlerts.featureRegistry.actionsFeatureName', { - defaultMessage: 'Stack Alerts', + defaultMessage: 'Stack Rules', }), app: [], category: DEFAULT_APP_CATEGORIES.management, diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 97eb9a9a05b86..c83935945c67b 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 70b4ba55c2393..c79b310822c3e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -56,7 +56,7 @@ export interface WorkloadAggregation { scheduleDensity: { range: { field: string; - ranges: [{ from: number; to: number }]; + ranges: [{ from: string; to: string }]; }; aggs: { histogram: { @@ -86,6 +86,7 @@ export interface WorkloadAggregation { // The type of a bucket in the scheduleDensity range aggregation type ScheduleDensityResult = AggregationResultOf< + // @ts-expect-error AggregationRange reqires from: number WorkloadAggregation['aggs']['idleTasks']['aggs']['scheduleDensity'], {} >['buckets'][0]; diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json index 95a098e54619e..a72b678da1f7c 100644 --- a/x-pack/plugins/task_manager/tsconfig.json +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index d924882e17fbd..aab1bdb58fe59 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -7,7 +7,7 @@ import { TelemetryCollectionXpackPlugin } from './plugin'; -export { ESLicense } from './telemetry_collection'; +export type { ESLicense } from './telemetry_collection'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index 4599b068b9b38..c1a11caf44f24 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ESLicense } from './get_license'; +export type { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 80488bd74617d..1221200c7548c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/**/*", diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index a9455877be429..3d5e1783f8c62 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -136,9 +136,20 @@ const apiFactory = () => ({ return Promise.resolve([]); }, async esSearch(payload: any): Promise { + const hits = []; + + // simulate a cross cluster search result + // against a cluster that doesn't support fields + if (payload.index.includes(':')) { + hits.push({ + _id: 'the-doc', + _index: 'the-index', + }); + } + return Promise.resolve({ hits: { - hits: [], + hits, total: { value: 0, relation: 'eq', diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index bd361afac2d8d..3e0a247106f2a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -7,7 +7,8 @@ import React, { FC } from 'react'; -import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { render, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { CoreSetup } from 'src/core/public'; @@ -49,6 +50,7 @@ describe('Transform: useIndexData()', () => { const wrapper: FC = ({ children }) => ( {children} ); + const { result, waitForNextUpdate } = renderHook( () => useIndexData( @@ -62,6 +64,7 @@ describe('Transform: useIndexData()', () => { ), { wrapper } ); + const IndexObj: UseIndexDataReturnType = result.current; await waitForNextUpdate(); @@ -73,7 +76,7 @@ describe('Transform: useIndexData()', () => { }); describe('Transform: with useIndexData()', () => { - test('Minimal initialization', async () => { + test('Minimal initialization, no cross cluster search warning.', async () => { // Arrange const indexPattern = { title: 'the-index-pattern-title', @@ -97,7 +100,47 @@ describe('Transform: with useIndexData()', () => { return ; }; - const { getByText } = render( + + const { queryByText } = render( + + + + ); + + // Act + // Assert + await waitFor(() => { + expect(queryByText('the-index-preview-title')).toBeInTheDocument(); + expect(queryByText('Cross-cluster search returned no fields data.')).not.toBeInTheDocument(); + }); + }); + + test('Cross-cluster search warning', async () => { + // Arrange + const indexPattern = { + title: 'remote:the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const mlSharedImports = await getMlSharedImports(); + + const Wrapper = () => { + const { + ml: { DataGrid }, + } = useAppDependencies(); + const props = { + ...useIndexData(indexPattern, { match_all: {} }, runtimeMappings), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return ; + }; + + const { queryByText } = render( @@ -105,6 +148,9 @@ describe('Transform: with useIndexData()', () => { // Act // Assert - expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByText('the-index-preview-title')).toBeInTheDocument(); + expect(queryByText('Cross-cluster search returned no fields data.')).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index bb83de8e12004..f97693b8c038a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -23,6 +23,7 @@ import { useApi } from './use_api'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common'; +import { isRuntimeMappings } from '../../../common/shared_imports'; export const useIndexData = ( indexPattern: SearchItems['indexPattern'], @@ -86,6 +87,7 @@ export const useIndexData = ( pagination, resetPagination, setColumnCharts, + setCcsWarning, setErrorMessage, setRowCount, setRowCountRelation, @@ -120,8 +122,7 @@ export const useIndexData = ( from: pagination.pageIndex * pagination.pageSize, size: pagination.pageSize, ...(Object.keys(sort).length > 0 ? { sort } : {}), - ...(typeof combinedRuntimeMappings === 'object' && - Object.keys(combinedRuntimeMappings).length > 0 + ...(isRuntimeMappings(combinedRuntimeMappings) ? { runtime_mappings: combinedRuntimeMappings } : {}), }, @@ -134,8 +135,12 @@ export const useIndexData = ( return; } + const isCrossClusterSearch = indexPattern.title.includes(':'); + const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined'); + const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + setCcsWarning(isCrossClusterSearch && isMissingFields); setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); setRowCountRelation( typeof resp.hits.total === 'number' @@ -189,7 +194,12 @@ export const useIndexData = ( } // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chartsVisible, indexPattern.title, JSON.stringify([query, dataGrid.visibleColumns])]); + }, [ + chartsVisible, + indexPattern.title, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings]), + ]); const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx index 9a026e839c731..7965db99b335b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx @@ -91,7 +91,7 @@ export const AdvancedRuntimeMappingsSettings: FC = (props) = defaultMessage: 'Runtime mappings', })} > - + {runtimeMappings !== undefined && Object.keys(runtimeMappings).length > 0 ? ( = (props) = - + {(copy: () => void) => ( = React.memo((props) => { } > <> - + {/* Flex Column #1: Search Bar / Advanced Search Editor */} {searchItems.savedSearch === undefined && ( diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 51d3a0bd02d50..1e3fa2026061b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, wait } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -77,7 +77,7 @@ describe('Transform: ', () => { }, }; - const { getByText } = render( + const { queryByText } = render( @@ -85,8 +85,9 @@ describe('Transform: ', () => { // Act // Assert - expect(getByText('Group by')).toBeInTheDocument(); - expect(getByText('Aggregations')).toBeInTheDocument(); - await wait(); + await waitFor(() => { + expect(queryByText('Group by')).toBeInTheDocument(); + expect(queryByText('Aggregations')).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 30da887cc1c43..2717f92c7a4df 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 133b4d0b6aaa8..14283aefd6149 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3080,7 +3080,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子はdata-updateに対応できるようこのメソッドを導入する必要があります", "maps_legacy.defaultDistributionMessage": "Mapsを入手するには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", - "maps_legacy.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。{ems}ではより多くのズームレベルを無料で利用できます。または、独自のマップサーバーを構成できます。詳細は、{ wms }または{ configSettings}をご覧ください。", "maps_legacy.legacyMapDeprecationMessage": "Mapsを使用すると、複数のレイヤーとインデックスを追加する、個別のドキュメントをプロットする、データ値から特徴を表現する、ヒートマップ、グリッド、クラスターを追加するなど、さまざまなことが可能です。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "{label}は8.0でMapsに移行されます。", "maps_legacy.openInMapsButtonLabel": "Mapsで表示", @@ -8170,10 +8169,6 @@ "xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "インデックス名またはパターンはすでに存在します。", "xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "インデックス名に許可されていない文字が含まれています。", "xpack.fileUpload.indexSettings.indexNameGuidelines": "インデックス名ガイドライン", - "xpack.fileUpload.jsonImport.indexingResponse": "インデックス応答", - "xpack.fileUpload.jsonImport.indexMgmtLink": "インデックス管理", - "xpack.fileUpload.jsonImport.indexModsMsg": "次を使用すると、その他のインデックス修正を行うことができます。\n", - "xpack.fileUpload.jsonImport.indexPatternResponse": "インデックスパターン応答", "xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "データインデックスエラー", "xpack.fileUpload.jsonUploadAndParse.indexPatternError": "インデックスパターンエラー", "xpack.fleet.agentBulkActions.clearSelection": "選択した項目をクリア", @@ -8731,12 +8726,8 @@ "xpack.fleet.upgradeAgents.cancelButtonLabel": "キャンセル", "xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "{count}個のエージェントをアップグレード", "xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "エージェントをアップグレード", - "xpack.fleet.upgradeAgents.deleteMultipleTitle": "{count} 個のエージェントをアップグレードしますか?", - "xpack.fleet.upgradeAgents.deleteSingleTitle": "エージェントをアップグレードしますか?", "xpack.fleet.upgradeAgents.experimentalLabel": "実験的", "xpack.fleet.upgradeAgents.experimentalLabelTooltip": "アップグレードエージェントは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", - "xpack.fleet.upgradeAgents.successMultiNotificationTitle": "エージェントをアップグレード中", - "xpack.fleet.upgradeAgents.successSingleNotificationTitle": "エージェントをアップグレード中", "xpack.fleet.upgradeAgents.upgradeMultipleDescription": "このアクションにより、複数のエージェントがバージョン{version}にアップグレードされます。このアップグレードは元に戻すことができません。", "xpack.fleet.upgradeAgents.upgradeSingleDescription": "このアクションにより、「{hostName}」で実行中のエージェントがバージョン{version}にアップグレードされます。このアップグレードは元に戻すことができません。", "xpack.globalSearch.find.invalidLicenseError": "GlobalSearch API は、ライセンス状態が無効であるため、無効になっています。{errorMessage}", @@ -13734,7 +13725,8 @@ "xpack.ml.editModelSnapshotFlyout.useDefaultButton": "削除", "xpack.ml.explorer.addToDashboard.cancelButtonLabel": "キャンセル", "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "ダッシュボードを選択:", - "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "スイムレーンビューを選択:", + "xpack.ml.explorer.addToDashboard.swimlanes.dashboardsTitle": "スイムレーンをダッシュボードに追加", + "xpack.ml.explorer.addToDashboard.swimlanes.selectSwimlanesLabel": "スイムレーンビューを選択:", "xpack.ml.explorer.addToDashboardLabel": "ダッシュボードに追加", "xpack.ml.explorer.annotationsErrorCallOutTitle": "注釈の読み込み中にエラーが発生しました。", "xpack.ml.explorer.annotationsErrorTitle": "注釈", @@ -13759,7 +13751,6 @@ "xpack.ml.explorer.dashboardsTable.descriptionColumnHeader": "説明", "xpack.ml.explorer.dashboardsTable.savedSuccessfullyTitle": "ダッシュボード「{dashboardTitle}」は正常に更新されました", "xpack.ml.explorer.dashboardsTable.titleColumnHeader": "タイトル", - "xpack.ml.explorer.dashboardsTitle": "スイムレーンをダッシュボードに追加", "xpack.ml.explorer.distributionChart.anomalyScoreLabel": "異常スコア", "xpack.ml.explorer.distributionChart.entityLabel": "エンティティ", "xpack.ml.explorer.distributionChart.typicalLabel": "通常", @@ -16661,9 +16652,6 @@ "xpack.observability.overview.logs.subtitle": "毎分のログレート", "xpack.observability.overview.logs.title": "ログ", "xpack.observability.overview.metrics.appLink": "アプリで表示", - "xpack.observability.overview.metrics.cpuUsage": "CPU 使用状況", - "xpack.observability.overview.metrics.hosts": "ホスト", - "xpack.observability.overview.metrics.memoryUsage": "メモリー使用状況", "xpack.observability.overview.metrics.title": "メトリック", "xpack.observability.overview.uptime.appLink": "アプリで表示", "xpack.observability.overview.uptime.chart.down": "ダウン", @@ -16773,10 +16761,6 @@ "xpack.remoteClusters.addTitle": "リモートクラスターを追加", "xpack.remoteClusters.appName": "リモートクラスター", "xpack.remoteClusters.appTitle": "リモートクラスター", - "xpack.remoteClusters.cloudClusterInformationDescription": "クラスターのプロキシアドレスとサーバー名を見つけるには、デプロイメニューの{security}ページに移動し、{searchString}を検索します。", - "xpack.remoteClusters.cloudClusterInformationTitle": "Elasticsearch Cloudデプロイのプロキシモードを使用", - "xpack.remoteClusters.cloudClusterSearchDescription": "リモートクラスターパラメーター", - "xpack.remoteClusters.cloudClusterSecurityDescription": "セキュリティ", "xpack.remoteClusters.configuredByNodeWarningTitle": "このリモートクラスターはノードの elasticsearch.yml 構成ファイルで定義されているため、編集または削除できません。", "xpack.remoteClusters.connectedStatus.connectedAriaLabel": "接続済み", "xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未接続", @@ -16850,7 +16834,6 @@ "xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "サーバー名", "xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "リモートクラスターごとに開くソケット接続の数。", "xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "リクエストを非表示", - "xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "「シードノード」フィールドが無効です。", "xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "「名前」フィールドが無効です。", "xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "「プロキシアドレス」フィールドが無効です。", "xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "「シードノード」フィールドが無効です。", @@ -19728,16 +19711,6 @@ "xpack.securitySolution.exceptions.addException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。作成された例外は、シーケンスのすべてのイベントに適用されます。", "xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました", "xpack.securitySolution.exceptions.andDescription": "AND", - "xpack.securitySolution.exceptions.builder.addNestedDescription": "ネストされた条件を追加", - "xpack.securitySolution.exceptions.builder.addNonNestedDescription": "ネストされていない条件を追加", - "xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "ネストされたフィールドを検索", - "xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "検索", - "xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "検索フィールド値...", - "xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "リストを検索...", - "xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "演算子", - "xpack.securitySolution.exceptions.builder.fieldDescription": "フィールド", - "xpack.securitySolution.exceptions.builder.operatorDescription": "演算子", - "xpack.securitySolution.exceptions.builder.valueDescription": "値", "xpack.securitySolution.exceptions.cancelLabel": "キャンセル", "xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", @@ -22925,7 +22898,6 @@ "xpack.uptime.certs.status.ok.label": " {okRelativeDate}", "xpack.uptime.charts.mlAnnotation.header": "スコア:{score}", "xpack.uptime.charts.mlAnnotation.severity": "深刻度:{severity}", - "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "オブザーバー位置情報マップを監視", "xpack.uptime.controls.selectSeverity.criticalLabel": "致命的", "xpack.uptime.controls.selectSeverity.majorLabel": "メジャー", "xpack.uptime.controls.selectSeverity.minorLabel": "マイナー", @@ -22955,13 +22927,7 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "{title} を検索", "xpack.uptime.filterPopover.filterItem.label": "{title} {item}でフィルタリングします。", "xpack.uptime.integrationLink.missingDataMessage": "この統合に必要なデータが見つかりませんでした。", - "xpack.uptime.kueryBar.indexPatternMissingWarningMessage": "インデックスパターンの取得中にエラーが発生しました。", - "xpack.uptime.locationAvailabilityViewToggleLegend": "トグルを表示", - "xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。", - "xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。", - "xpack.uptime.locationMap.locations.missing.title": "地理情報の欠測", "xpack.uptime.locationName.helpLinkAnnotation": "場所を追加", - "xpack.uptime.mapToolTip.AvailabilityStat.title": "{value} %", "xpack.uptime.ml.durationChart.exploreInMlApp": "ML アプリで探索", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "異常検知", "xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel": "キャンセル", @@ -23095,9 +23061,6 @@ "xpack.uptime.overviewPageLink.disabled.ariaLabel": "無効になったページ付けボタンです。モニターリストがこれ以上ナビゲーションできないことを示しています。", "xpack.uptime.overviewPageLink.next.ariaLabel": "次の結果ページ", "xpack.uptime.overviewPageLink.prev.ariaLabel": "前の結果ページ", - "xpack.uptime.overviewPageParsingErrorCallout.content": "フィルタークエリの解析中にエラーが発生しました。{content}", - "xpack.uptime.overviewPageParsingErrorCallout.noMessage": "エラーメッセージはありませんでした", - "xpack.uptime.overviewPageParsingErrorCallout.title": "エラーを解析中", "xpack.uptime.page_header.settingsLink": "設定", "xpack.uptime.pingist.durationSecondsColumnFormatting": "{seconds}秒", "xpack.uptime.pingist.durationSecondsColumnFormatting.singular": "{seconds}秒", @@ -23616,4 +23579,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0f9d8b90a2578..50a3aeb5e0c44 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3101,7 +3101,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子对象应实现此方法以响应数据更新", "maps_legacy.defaultDistributionMessage": "要获取 Maps,请升级到 {defaultDistribution} 版的 Elasticsearch 和 Kibana。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", - "maps_legacy.kibanaMap.zoomWarning": "已达到缩放级别数目上限。要一直放大,请升级到 Elasticsearch 和 Kibana 的{defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", "maps_legacy.legacyMapDeprecationMessage": "使用 Maps,可以添加多个图层和索引,绘制单个文档,使用数据值表示特征,添加热图、网格和集群,等等。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "在 8.0 中,{label} 将迁移到 Maps。", "maps_legacy.openInMapsButtonLabel": "在 Maps 中查看", @@ -8243,10 +8242,6 @@ "xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "索引名称或模式已存在。", "xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "索引名称包含非法字符。", "xpack.fileUpload.indexSettings.indexNameGuidelines": "索引名称指引", - "xpack.fileUpload.jsonImport.indexingResponse": "索引响应", - "xpack.fileUpload.jsonImport.indexMgmtLink": "索引管理", - "xpack.fileUpload.jsonImport.indexModsMsg": "要进一步做索引修改,可以使用\n", - "xpack.fileUpload.jsonImport.indexPatternResponse": "索引模式响应", "xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "数据索引错误", "xpack.fileUpload.jsonUploadAndParse.indexPatternError": "索引模式错误", "xpack.fleet.agentBulkActions.agentsSelected": "已选择 {count, plural, other {# 个代理}}", @@ -8819,13 +8814,9 @@ "xpack.fleet.upgradeAgents.cancelButtonLabel": "取消", "xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "升级 {count} 个代理", "xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "升级代理", - "xpack.fleet.upgradeAgents.deleteMultipleTitle": "升级 {count} 个代理?", - "xpack.fleet.upgradeAgents.deleteSingleTitle": "升级代理?", "xpack.fleet.upgradeAgents.experimentalLabel": "实验性", "xpack.fleet.upgradeAgents.experimentalLabelTooltip": "在未来的版本中可能会更改或移除升级代理,其不受支持 SLA 的约束。", "xpack.fleet.upgradeAgents.fatalErrorNotificationTitle": "升级{count, plural, other {代理}}时出错", - "xpack.fleet.upgradeAgents.successMultiNotificationTitle": "正在升级代理", - "xpack.fleet.upgradeAgents.successSingleNotificationTitle": "正在升级代理", "xpack.fleet.upgradeAgents.upgradeMultipleDescription": "此操作将多个代理升级到版本 {version}。您无法撤消此升级。", "xpack.fleet.upgradeAgents.upgradeSingleDescription": "此操作会将运行在“{hostName}”上的代理升级到版本 {version}。您无法撤消此升级。", "xpack.globalSearch.find.invalidLicenseError": "GlobalSearch API 已禁用,因为许可状态无效:{errorMessage}", @@ -13914,7 +13905,8 @@ "xpack.ml.editModelSnapshotFlyout.useDefaultButton": "删除", "xpack.ml.explorer.addToDashboard.cancelButtonLabel": "取消", "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "选择仪表板:", - "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "选择泳道视图:", + "xpack.ml.explorer.addToDashboard.swimlanes.dashboardsTitle": "将泳道添加到仪表板", + "xpack.ml.explorer.addToDashboard.swimlanes.selectSwimlanesLabel": "选择泳道视图:", "xpack.ml.explorer.addToDashboardLabel": "添加到仪表板", "xpack.ml.explorer.annotationsErrorCallOutTitle": "加载注释时发生错误:", "xpack.ml.explorer.annotationsErrorTitle": "标注", @@ -13939,7 +13931,6 @@ "xpack.ml.explorer.dashboardsTable.descriptionColumnHeader": "描述", "xpack.ml.explorer.dashboardsTable.savedSuccessfullyTitle": "仪表板“{dashboardTitle}”已成功更新", "xpack.ml.explorer.dashboardsTable.titleColumnHeader": "标题", - "xpack.ml.explorer.dashboardsTitle": "将泳道添加到仪表板", "xpack.ml.explorer.distributionChart.anomalyScoreLabel": "异常分数", "xpack.ml.explorer.distributionChart.entityLabel": "实体", "xpack.ml.explorer.distributionChart.typicalLabel": "典型", @@ -16887,9 +16878,6 @@ "xpack.observability.overview.logs.subtitle": "每分钟日志速率", "xpack.observability.overview.logs.title": "日志", "xpack.observability.overview.metrics.appLink": "在应用中查看", - "xpack.observability.overview.metrics.cpuUsage": "CPU 使用", - "xpack.observability.overview.metrics.hosts": "主机", - "xpack.observability.overview.metrics.memoryUsage": "内存使用", "xpack.observability.overview.metrics.title": "指标", "xpack.observability.overview.uptime.appLink": "在应用中查看", "xpack.observability.overview.uptime.chart.down": "关闭", @@ -16999,10 +16987,6 @@ "xpack.remoteClusters.addTitle": "添加远程集群", "xpack.remoteClusters.appName": "远程集群", "xpack.remoteClusters.appTitle": "远程集群", - "xpack.remoteClusters.cloudClusterInformationDescription": "要查找您的集群的代理地址和服务器名称,请前往部署菜单的{security}页面并搜索“{searchString}”。", - "xpack.remoteClusters.cloudClusterInformationTitle": "将代理模式用于 Elastic Cloud 部署", - "xpack.remoteClusters.cloudClusterSearchDescription": "远程集群参数", - "xpack.remoteClusters.cloudClusterSecurityDescription": "安全", "xpack.remoteClusters.configuredByNodeWarningTitle": "您无法编辑或删除此远程集群,因为它是在节点的 elasticsearch.yml 配置文件中定义的。", "xpack.remoteClusters.connectedStatus.connectedAriaLabel": "已连接", "xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未连接", @@ -17077,7 +17061,6 @@ "xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "服务器名", "xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "每个远程集群要打开的套接字数目。", "xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "隐藏请求", - "xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "“种子节点”字段无效。", "xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "“名称”字段无效。", "xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "“代理地址”字段无效。", "xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "“种子节点”字段无效。", @@ -20023,16 +20006,6 @@ "xpack.securitySolution.exceptions.addException.sequenceWarning": "此规则的查询包含 EQL 序列语句。创建的例外将应用于序列中的所有事件。", "xpack.securitySolution.exceptions.addException.success": "已成功添加例外", "xpack.securitySolution.exceptions.andDescription": "AND", - "xpack.securitySolution.exceptions.builder.addNestedDescription": "添加嵌套条件", - "xpack.securitySolution.exceptions.builder.addNonNestedDescription": "添加非嵌套条件", - "xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "搜索嵌套字段", - "xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "搜索", - "xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "搜索字段值......", - "xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "搜索列表......", - "xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "运算符", - "xpack.securitySolution.exceptions.builder.fieldDescription": "字段", - "xpack.securitySolution.exceptions.builder.operatorDescription": "运算符", - "xpack.securitySolution.exceptions.builder.valueDescription": "值", "xpack.securitySolution.exceptions.cancelLabel": "取消", "xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", @@ -23284,7 +23257,6 @@ "xpack.uptime.certs.status.ok.label": " 对于 {okRelativeDate}", "xpack.uptime.charts.mlAnnotation.header": "分数:{score}", "xpack.uptime.charts.mlAnnotation.severity": "严重性:{severity}", - "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "监测观察者位置地图", "xpack.uptime.controls.selectSeverity.criticalLabel": "紧急", "xpack.uptime.controls.selectSeverity.majorLabel": "重大", "xpack.uptime.controls.selectSeverity.minorLabel": "轻微", @@ -23314,13 +23286,7 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "搜索 {title}", "xpack.uptime.filterPopover.filterItem.label": "按 {title} {item} 筛选。", "xpack.uptime.integrationLink.missingDataMessage": "未找到此集成的所需数据。", - "xpack.uptime.kueryBar.indexPatternMissingWarningMessage": "检索索引模式时出错。", - "xpack.uptime.locationAvailabilityViewToggleLegend": "视图切换", - "xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。", - "xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。", - "xpack.uptime.locationMap.locations.missing.title": "地理信息缺失", "xpack.uptime.locationName.helpLinkAnnotation": "添加位置", - "xpack.uptime.mapToolTip.AvailabilityStat.title": "{value} %", "xpack.uptime.ml.durationChart.exploreInMlApp": "在 ML 应用中浏览", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "异常检测", "xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel": "取消", @@ -23454,9 +23420,6 @@ "xpack.uptime.overviewPageLink.disabled.ariaLabel": "禁用的分页按钮表示在监测列表中无法进行进一步导航。", "xpack.uptime.overviewPageLink.next.ariaLabel": "下页结果", "xpack.uptime.overviewPageLink.prev.ariaLabel": "上页结果", - "xpack.uptime.overviewPageParsingErrorCallout.content": "解析筛选查询时出错。{content}", - "xpack.uptime.overviewPageParsingErrorCallout.noMessage": "没有错误消息", - "xpack.uptime.overviewPageParsingErrorCallout.title": "解析错误", "xpack.uptime.page_header.settingsLink": "设置", "xpack.uptime.pingist.durationSecondsColumnFormatting": "{seconds} 秒", "xpack.uptime.pingist.durationSecondsColumnFormatting.singular": "{seconds} 秒", @@ -23985,4 +23948,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx index 8d27edd9e4bcc..4e03a2a09bed4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx @@ -117,4 +117,16 @@ describe('AddMessageVariables', () => { wrapper.find('button[data-test-subj="variableMenuButton-deprecatedVar"]').getDOMNode() ).toBeDisabled(); }); + + test(`it does't render when no variables exist`, () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="fooAddVariableButton"]')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index bf89e4f6ae6e1..57b251fba0d45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, @@ -61,13 +61,16 @@ export const AddMessageVariables: React.FunctionComponent = ({ } ); + if ((messageVariables?.length ?? 0) === 0) { + return ; + } + return ( setIsVariablesPopoverOpen(true)} iconType="indexOpen" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx index 008fc8237c129..e9212bf633a79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { act } from 'react-dom/test-utils'; import { EsIndexActionConnector } from '../types'; import IndexActionConnectorFields from './es_index_connector'; +import { EuiComboBox, EuiSwitch, EuiSwitchEvent, EuiSelect } from '@elastic/eui'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/index_controls', () => ({ @@ -19,83 +20,263 @@ jest.mock('../../../../common/index_controls', () => ({ getIndexPatterns: jest.fn(), })); +const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); +getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, +]); + +const { getFields } = jest.requireMock('../../../../common/index_controls'); + +async function setup(props: any) { + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; +} + +function setupGetFieldsResponse(getFieldsWithDateMapping: boolean) { + getFields.mockResolvedValueOnce([ + { + type: getFieldsWithDateMapping ? 'date' : 'keyword', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); +} describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); - getIndexPatterns.mockResolvedValueOnce([ - { - id: 'indexPattern1', - attributes: { - title: 'indexPattern1', - }, - }, - { - id: 'indexPattern2', - attributes: { - title: 'indexPattern2', - }, - }, - ]); - const { getFields } = jest.requireMock('../../../../common/index_controls'); - getFields.mockResolvedValueOnce([ - { - type: 'date', - name: 'test1', - }, - { - type: 'text', - name: 'test2', - }, - ]); - - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test', - refresh: false, - executionTimeField: 'test1', - }, - } as EsIndexActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - /> - ); + test('renders correctly when creating connector', async () => { + const props = { + action: { + actionTypeId: '.index', + config: {}, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + + // time field switch shouldn't show up initially + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + + // time field switch should show up if index has date type field mapping + setupGetFieldsResponse(true); await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + // time field switch should go away if index does not has date type field mapping + setupGetFieldsResponse(false); + await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + // time field dropdown should show up if index has date type field mapping and time switch is clicked + setupGetFieldsResponse(true); + await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + await act(async () => { + timeFieldSwitch.prop('onChange')!(({ + target: { checked: true }, + } as unknown) as EuiSwitchEvent); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + }); + + test('renders correctly when editing connector - no date type field mapping', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(false); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + + // time related fields shouldn't show up + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); + }); + + test('renders correctly when editing connector - refresh set to true', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: true, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(false); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(true); + }); + + test('renders correctly when editing connector - with date type field mapping but no time field selected', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(true); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); + + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + expect(timeFieldSwitch.prop('checked')).toEqual(false); + }); + + test('renders correctly when editing connector - with date type field mapping and selected time field', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + executionTimeField: 'test1', + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(true); + const wrapper = await setup(props); - const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); - expect(indexSearchBoxValue.first().props().value).toEqual(''); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); - const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); - indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox.find('input').first().simulate('change', event); + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); - const indexSearchBoxValueBeforeEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); - const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); - indexComboBoxClear.first().simulate('click'); + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + expect(timeFieldSwitch.prop('checked')).toEqual(true); - const indexSearchBoxValueAfterEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); + const timeFieldSelect = wrapper + .find(EuiSelect) + .filter('[data-test-subj="executionTimeFieldSelect"]'); + expect(timeFieldSelect.prop('value')).toEqual('test1'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index cd3a03ecce15c..72af41277c29c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -30,29 +30,45 @@ import { } from '../../../../common/index_controls'; import { useKibana } from '../../../../common/lib/kibana'; +interface TimeFieldOptions { + value: string; + text: string; +} + const IndexActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, errors, readOnly }) => { const { http, docLinks } = useKibana().services; const { index, refresh, executionTimeField } = action.config; - const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState( + const [showTimeFieldCheckbox, setShowTimeFieldCheckboxState] = useState( + executionTimeField != null + ); + const [hasTimeFieldCheckbox, setHasTimeFieldCheckboxState] = useState( executionTimeField != null ); const [indexPatterns, setIndexPatterns] = useState([]); const [indexOptions, setIndexOptions] = useState([]); - const [timeFieldOptions, setTimeFieldOptions] = useState>([ - firstFieldOption, - ]); + const [timeFieldOptions, setTimeFieldOptions] = useState([]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); + const setTimeFields = (fields: TimeFieldOptions[]) => { + if (fields.length > 0) { + setShowTimeFieldCheckboxState(true); + setTimeFieldOptions([firstFieldOption, ...fields]); + } else { + setHasTimeFieldCheckboxState(false); + setShowTimeFieldCheckboxState(false); + setTimeFieldOptions([]); + } + }; + useEffect(() => { const indexPatternsFunction = async () => { setIndexPatterns(await getIndexPatterns()); if (index) { const currentEsFields = await getFields(http!, [index]); - const timeFields = getTimeFieldOptions(currentEsFields as any); - setTimeFieldOptions([firstFieldOption, ...timeFields]); + setTimeFields(getTimeFieldOptions(currentEsFields as any)); } }; indexPatternsFunction(); @@ -123,13 +139,11 @@ const IndexActionConnectorFields: React.FunctionComponent< // reset time field and expression fields if indices are deleted if (indices.length === 0) { - setTimeFieldOptions([]); + setTimeFields([]); return; } const currentEsFields = await getFields(http!, indices); - const timeFields = getTimeFieldOptions(currentEsFields as any); - - setTimeFieldOptions([firstFieldOption, ...timeFields]); + setTimeFields(getTimeFieldOptions(currentEsFields as any)); }} onSearchChange={async (search) => { setIsIndiciesLoading(true); @@ -172,38 +186,40 @@ const IndexActionConnectorFields: React.FunctionComponent< } /> - { - setTimeFieldCheckboxState(!hasTimeFieldCheckbox); - // if changing from checked to not checked (hasTimeField === true), - // set time field to null - if (hasTimeFieldCheckbox) { - editActionConfig('executionTimeField', null); + {showTimeFieldCheckbox && ( + { + setHasTimeFieldCheckboxState(!hasTimeFieldCheckbox); + // if changing from checked to not checked (hasTimeField === true), + // set time field to null + if (hasTimeFieldCheckbox) { + editActionConfig('executionTimeField', null); + } + }} + label={ + <> + + + } - }} - label={ - <> - - - - } - /> - {hasTimeFieldCheckbox ? ( + /> + )} + {hasTimeFieldCheckbox && ( <> - ) : null} + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 97c1c41f68730..b792cf6574455 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -22,6 +22,13 @@ describe('IndexParamsFields renders', () => { errors={{ index: [] }} editAction={() => {}} index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} /> ); expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(`{ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index 6e3f4213b7907..4d47cbf3685a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -30,6 +30,13 @@ describe('PagerDutyParamsFields renders', () => { errors={{ summary: [], timestamp: [], dedupKey: [] }} editAction={() => {}} index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} /> ); expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx index 801d9a6b43ec6..a3756ae74fd14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -21,6 +21,13 @@ describe('WebhookParamsFields renders', () => { errors={{ body: [] }} editAction={() => {}} index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} /> ); expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 3baf4e33fb68d..44c950a500040 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -59,8 +59,13 @@ describe('health check', () => { it('renders children if keys are enabled', async () => { useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, + is_sufficiently_secure: true, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, }); const { queryByText } = render( @@ -78,8 +83,13 @@ describe('health check', () => { test('renders warning if TLS is required', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: false, - hasPermanentEncryptionKey: true, + is_sufficiently_secure: false, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); const { queryAllByText } = render( @@ -110,8 +120,13 @@ describe('health check', () => { test('renders warning if encryption key is ephemeral', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: false, + is_sufficiently_secure: true, + has_permanent_encryption_key: false, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); const { queryByText, queryByRole } = render( @@ -139,8 +154,13 @@ describe('health check', () => { test('renders warning if encryption key is ephemeral and keys are disabled', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: false, - hasPermanentEncryptionKey: false, + is_sufficiently_secure: false, + has_permanent_encryption_key: false, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 208fd5ec66f1d..d75ab102a8e0c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -15,12 +15,12 @@ import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { DocLinksStart } from 'kibana/public'; -import { alertingFrameworkHealth } from '../lib/alert_api'; import './health_check.scss'; import { useHealthContext } from '../context/health_context'; import { useKibana } from '../../common/lib/kibana'; import { CenterJustifiedSpinner } from './center_justified_spinner'; import { triggersActionsUiHealth } from '../../common/lib/health_api'; +import { alertingFrameworkHealth } from '../lib/alert_api'; interface Props { inFlyout?: boolean; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 8ac1fbaec403b..cc04b8e7871cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; -export { LEGACY_BASE_ALERT_API_PATH } from '../../../../alerting/common'; +export { + BASE_ALERTING_API_PATH, + INTERNAL_BASE_ALERTING_API_PATH, +} from '../../../../alerting/common'; export { BASE_ACTION_API_PATH } from '../../../../actions/common'; export type Section = 'connectors' | 'rules'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts deleted file mode 100644 index d112e7ac284ae..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ /dev/null @@ -1,875 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Alert, AlertType, AlertUpdates } from '../../types'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; -import { - createAlert, - deleteAlerts, - disableAlerts, - enableAlerts, - disableAlert, - enableAlert, - loadAlert, - loadAlertAggregations, - loadAlerts, - loadAlertState, - loadAlertTypes, - muteAlerts, - unmuteAlerts, - muteAlert, - unmuteAlert, - updateAlert, - muteAlertInstance, - unmuteAlertInstance, - alertingFrameworkHealth, - mapFiltersToKql, -} from './alert_api'; -import uuid from 'uuid'; -import { AlertNotifyWhenType, ALERTS_FEATURE_ID } from '../../../../alerting/common'; - -const http = httpServiceMock.createStartContract(); - -beforeEach(() => jest.resetAllMocks()); - -describe('loadAlertTypes', () => { - test('should call get alert types API', async () => { - const resolvedValue: AlertType[] = [ - { - id: 'test', - name: 'Test', - actionVariables: { - context: [{ name: 'var1', description: 'val1' }], - state: [{ name: 'var2', description: 'val2' }], - params: [{ name: 'var3', description: 'val3' }], - }, - producer: ALERTS_FEATURE_ID, - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - defaultActionGroupId: 'default', - authorizedConsumers: {}, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }, - ]; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertTypes({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/list_alert_types", - ] - `); - }); -}); - -describe('loadAlert', () => { - test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - id: alertId, - name: 'name', - tags: [], - enabled: true, - alertTypeId: '.noop', - schedule: { interval: '1s' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); - }); -}); - -describe('loadAlertState', () => { - test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: {}, - second_instance: {}, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); - - test('should parse AlertInstances', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: '2020-02-09T23:15:41.941Z', - }, - }, - }, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlertState({ http, alertId })).toEqual({ - ...resolvedValue, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: new Date('2020-02-09T23:15:41.941Z'), - }, - }, - }, - }, - }); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); - - test('should handle empty response from api', async () => { - const alertId = uuid.v4(); - http.get.mockResolvedValueOnce(''); - - expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); -}); - -describe('loadAlerts', () => { - test('should call find API with base parameters', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with searchText', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with actionTypesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'foo', - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with actionTypesFilter and typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'baz', - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with searchText and tagsFilter and typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'apples, foo, baz', - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "apples, foo, baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); -}); - -describe('loadAlertAggregations', () => { - test('should call aggregate API with base parameters', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); - }); - - test('should call aggregate API with searchText', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ http, searchText: 'apples' }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); - - test('should call aggregate API with actionTypesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - searchText: 'foo', - actionTypesFilter: ['action', 'type'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); - - test('should call aggregate API with typesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - typesFilter: ['foo', 'bar'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); - }); - - test('should call aggregate API with actionTypesFilter and typesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - searchText: 'baz', - actionTypesFilter: ['action', 'type'], - typesFilter: ['foo', 'bar'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); -}); - -describe('deleteAlerts', () => { - test('should call delete API for each alert', async () => { - const ids = ['1', '2', '3']; - const result = await deleteAlerts({ http, ids }); - expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); - expect(http.delete.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1", - ], - Array [ - "/api/alerts/alert/2", - ], - Array [ - "/api/alerts/alert/3", - ], - ] - `); - }); -}); - -describe('createAlert', () => { - test('should call create alert API', async () => { - const alertToCreate: AlertUpdates = { - name: 'test', - consumer: 'alerts', - tags: ['foo'], - enabled: true, - alertTypeId: 'test', - schedule: { - interval: '1m', - }, - actions: [], - params: {}, - throttle: null, - notifyWhen: 'onActionGroupChange' as AlertNotifyWhenType, - createdAt: new Date('1970-01-01T00:00:00.000Z'), - updatedAt: new Date('1970-01-01T00:00:00.000Z'), - apiKeyOwner: null, - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - }; - const resolvedValue = { - ...alertToCreate, - id: '123', - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - }; - http.post.mockResolvedValueOnce(resolvedValue); - - const result = await createAlert({ http, alert: alertToCreate }); - expect(result).toEqual(resolvedValue); - expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/alert", - Object { - "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"notifyWhen\\":\\"onActionGroupChange\\",\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKeyOwner\\":null,\\"createdBy\\":null,\\"updatedBy\\":null,\\"muteAll\\":false,\\"mutedInstanceIds\\":[]}", - }, - ] - `); - }); -}); - -describe('updateAlert', () => { - test('should call alert update API', async () => { - const alertToUpdate = { - throttle: '1m', - consumer: 'alerts', - name: 'test', - tags: ['foo'], - schedule: { - interval: '1m', - }, - params: {}, - actions: [], - createdAt: new Date('1970-01-01T00:00:00.000Z'), - updatedAt: new Date('1970-01-01T00:00:00.000Z'), - apiKey: null, - apiKeyOwner: null, - notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, - }; - const resolvedValue: Alert = { - ...alertToUpdate, - id: '123', - enabled: true, - alertTypeId: 'test', - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - }; - http.put.mockResolvedValueOnce(resolvedValue); - - const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); - expect(result).toEqual(resolvedValue); - expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/alert/123", - Object { - "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[],\\"notifyWhen\\":\\"onThrottleInterval\\"}", - }, - ] - `); - }); -}); - -describe('enableAlert', () => { - test('should call enable alert API', async () => { - const result = await enableAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_enable", - ], - ] - `); - }); -}); - -describe('disableAlert', () => { - test('should call disable alert API', async () => { - const result = await disableAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_disable", - ], - ] - `); - }); -}); - -describe('muteAlertInstance', () => { - test('should call mute instance alert API', async () => { - const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/alert_instance/123/_mute", - ], - ] - `); - }); -}); - -describe('unmuteAlertInstance', () => { - test('should call mute instance alert API', async () => { - const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/alert_instance/123/_unmute", - ], - ] - `); - }); -}); - -describe('muteAlert', () => { - test('should call mute alert API', async () => { - const result = await muteAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_mute_all", - ], - ] - `); - }); -}); - -describe('unmuteAlert', () => { - test('should call unmute alert API', async () => { - const result = await unmuteAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_unmute_all", - ], - ] - `); - }); -}); - -describe('enableAlerts', () => { - test('should call enable alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await enableAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_enable", - ], - Array [ - "/api/alerts/alert/2/_enable", - ], - Array [ - "/api/alerts/alert/3/_enable", - ], - ] - `); - }); -}); - -describe('disableAlerts', () => { - test('should call disable alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await disableAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_disable", - ], - Array [ - "/api/alerts/alert/2/_disable", - ], - Array [ - "/api/alerts/alert/3/_disable", - ], - ] - `); - }); -}); - -describe('muteAlerts', () => { - test('should call mute alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await muteAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_mute_all", - ], - Array [ - "/api/alerts/alert/2/_mute_all", - ], - Array [ - "/api/alerts/alert/3/_mute_all", - ], - ] - `); - }); -}); - -describe('unmuteAlerts', () => { - test('should call unmute alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await unmuteAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_unmute_all", - ], - Array [ - "/api/alerts/alert/2/_unmute_all", - ], - Array [ - "/api/alerts/alert/3/_unmute_all", - ], - ] - `); - }); -}); - -describe('alertingFrameworkHealth', () => { - test('should call alertingFrameworkHealth API', async () => { - const result = await alertingFrameworkHealth({ http }); - expect(result).toEqual(undefined); - expect(http.get.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/_health", - ], - ] - `); - }); -}); - -describe('mapFiltersToKql', () => { - test('should handle no filters', () => { - expect(mapFiltersToKql({})).toEqual([]); - }); - - test('should handle typesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - }) - ).toEqual(['alert.attributes.alertTypeId:(type or filter)']); - }); - - test('should handle actionTypesFilter', () => { - expect( - mapFiltersToKql({ - actionTypesFilter: ['action', 'types', 'filter'], - }) - ).toEqual([ - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - ]); - }); - - test('should handle alertStatusesFilter', () => { - expect( - mapFiltersToKql({ - alertStatusesFilter: ['alert', 'statuses', 'filter'], - }) - ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); - }); - - test('should handle typesFilter and actionTypesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - actionTypesFilter: ['action', 'types', 'filter'], - }) - ).toEqual([ - 'alert.attributes.alertTypeId:(type or filter)', - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - ]); - }); - - test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - actionTypesFilter: ['action', 'types', 'filter'], - alertStatusesFilter: ['alert', 'statuses', 'filter'], - }) - ).toEqual([ - 'alert.attributes.alertTypeId:(type or filter)', - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - 'alert.attributes.executionStatus.status:(alert or statuses or filter)', - ]); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts deleted file mode 100644 index 80ff415582191..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ /dev/null @@ -1,296 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HttpSetup } from 'kibana/public'; -import { Errors, identity } from 'io-ts'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { pick } from 'lodash'; -import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerting/common'; -import { LEGACY_BASE_ALERT_API_PATH } from '../constants'; -import { - Alert, - AlertAggregations, - AlertType, - AlertUpdates, - AlertTaskState, - AlertInstanceSummary, - Pagination, - Sorting, -} from '../../types'; - -export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/list_alert_types`); -} - -export async function loadAlert({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}`); -} - -type EmptyHttpResponse = ''; -export async function loadAlertState({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http - .get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}/state`) - .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) - .then((state: AlertTaskState) => { - return pipe( - alertStateSchema.decode(state), - fold((e: Errors) => { - throw new Error(`Alert "${alertId}" has invalid state`); - }, identity) - ); - }); -} - -export async function loadAlertInstanceSummary({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}/_instance_summary`); -} - -export const mapFiltersToKql = ({ - typesFilter, - actionTypesFilter, - alertStatusesFilter, -}: { - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): string[] => { - const filters = []; - if (typesFilter && typesFilter.length) { - filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); - } - if (actionTypesFilter && actionTypesFilter.length) { - filters.push( - [ - '(', - actionTypesFilter - .map((id) => `alert.attributes.actions:{ actionTypeId:${id} }`) - .join(' OR '), - ')', - ].join('') - ); - } - if (alertStatusesFilter && alertStatusesFilter.length) { - filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); - } - return filters; -}; - -export async function loadAlerts({ - http, - page, - searchText, - typesFilter, - actionTypesFilter, - alertStatusesFilter, - sort = { field: 'name', direction: 'asc' }, -}: { - http: HttpSetup; - page: Pagination; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; - sort?: Sorting; -}): Promise<{ - page: number; - perPage: number; - total: number; - data: Alert[]; -}> { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_find`, { - query: { - page: page.index + 1, - per_page: page.size, - search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, - search: searchText, - filter: filters.length ? filters.join(' and ') : undefined, - default_search_operator: 'AND', - sort_field: sort.field, - sort_order: sort.direction, - }, - }); -} - -export async function loadAlertAggregations({ - http, - searchText, - typesFilter, - actionTypesFilter, - alertStatusesFilter, -}: { - http: HttpSetup; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): Promise { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_aggregate`, { - query: { - search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, - search: searchText, - filter: filters.length ? filters.join(' and ') : undefined, - default_search_operator: 'AND', - }, - }); -} - -export async function deleteAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise<{ successes: string[]; errors: string[] }> { - const successes: string[] = []; - const errors: string[] = []; - await Promise.all(ids.map((id) => http.delete(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`))).then( - function (fulfilled) { - successes.push(...fulfilled); - }, - function (rejected) { - errors.push(...rejected); - } - ); - return { successes, errors }; -} - -export async function createAlert({ - http, - alert, -}: { - http: HttpSetup; - alert: Omit< - AlertUpdates, - 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' - >; -}): Promise { - return await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert`, { - body: JSON.stringify(alert), - }); -} - -export async function updateAlert({ - http, - alert, - id, -}: { - http: HttpSetup; - alert: Pick< - AlertUpdates, - 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' - >; - id: string; -}): Promise { - return await http.put(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`, { - body: JSON.stringify( - pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) - ), - }); -} - -export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_enable`); -} - -export async function enableAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => enableAlert({ id, http }))); -} - -export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_disable`); -} - -export async function disableAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => disableAlert({ id, http }))); -} - -export async function muteAlertInstance({ - id, - instanceId, - http, -}: { - id: string; - instanceId: string; - http: HttpSetup; -}): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_mute`); -} - -export async function unmuteAlertInstance({ - id, - instanceId, - http, -}: { - id: string; - instanceId: string; - http: HttpSetup; -}): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_unmute`); -} - -export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_mute_all`); -} - -export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map((id) => muteAlert({ http, id }))); -} - -export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_unmute_all`); -} - -export async function unmuteAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); -} - -export async function alertingFrameworkHealth({ - http, -}: { - http: HttpSetup; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_health`); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts new file mode 100644 index 0000000000000..57feb1e7abae9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertAggregations } from './aggregate'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertAggregations', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call aggregate API with base parameters', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ http }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); + + test('should call aggregate API with searchText', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ http, searchText: 'apples' }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('should call aggregate API with actionTypesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + searchText: 'foo', + actionTypesFilter: ['action', 'type'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('should call aggregate API with typesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + typesFilter: ['foo', 'bar'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); + + test('should call aggregate API with actionTypesFilter and typesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + searchText: 'baz', + actionTypesFilter: ['action', 'type'], + typesFilter: ['foo', 'bar'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts new file mode 100644 index 0000000000000..589677ec2322d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AlertAggregations } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { mapFiltersToKql } from './map_filters_to_kql'; +import { RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + rule_execution_status: alertExecutionStatus, + ...rest +}: any) => ({ + ...rest, + alertExecutionStatus, +}); + +export async function loadAlertAggregations({ + http, + searchText, + typesFilter, + actionTypesFilter, + alertStatusesFilter, +}: { + http: HttpSetup; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; +}): Promise { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { + query: { + search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, + search: searchText, + filter: filters.length ? filters.join(' and ') : undefined, + default_search_operator: 'AND', + }, + }); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts new file mode 100644 index 0000000000000..e94da81d0f5d5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AlertInstanceSummary } from '../../../../../alerting/common'; +import { loadAlertInstanceSummary } from './alert_summary'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertInstanceSummary', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertInstanceSummary = { + instances: {}, + consumer: 'alerts', + enabled: true, + errorMessages: [], + id: 'test', + lastRun: '2021-04-01T22:18:27.609Z', + muteAll: false, + name: 'test', + alertTypeId: '.index-threshold', + status: 'OK', + statusEndDate: '2021-04-01T22:19:25.174Z', + statusStartDate: '2021-04-01T21:19:25.174Z', + tags: [], + throttle: null, + }; + + http.get.mockResolvedValueOnce({ + alerts: {}, + consumer: 'alerts', + enabled: true, + error_messages: [], + id: 'test', + last_run: '2021-04-01T22:18:27.609Z', + mute_all: false, + name: 'test', + rule_type_id: '.index-threshold', + status: 'OK', + status_end_date: '2021-04-01T22:19:25.174Z', + status_start_date: '2021-04-01T21:19:25.174Z', + tags: [], + throttle: null, + }); + + const result = await loadAlertInstanceSummary({ http, alertId: 'test' }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rule/test/_alert_summary", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts new file mode 100644 index 0000000000000..e37c0640ec1c8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { AlertInstanceSummary } from '../../../types'; +import { RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + alerts, + rule_type_id: alertTypeId, + mute_all: muteAll, + status_start_date: statusStartDate, + status_end_date: statusEndDate, + error_messages: errorMessages, + last_run: lastRun, + ...rest +}: any) => ({ + ...rest, + alertTypeId, + muteAll, + statusStartDate, + statusEndDate, + errorMessages, + lastRun, + instances: alerts, +}); + +export async function loadAlertInstanceSummary({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/_alert_summary`); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts new file mode 100644 index 0000000000000..749cf53cf740b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertExecutionStatus } from '../../../../../alerting/common'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { Alert, AlertAction } from '../../../types'; + +const transformAction: RewriteRequestCase = ({ + group, + id, + connector_type_id: actionTypeId, + params, +}) => ({ + group, + id, + params, + actionTypeId, +}); + +const transformExecutionStatus: RewriteRequestCase = ({ + last_execution_date: lastExecutionDate, + ...rest +}) => ({ + lastExecutionDate, + ...rest, +}); + +export const transformAlert: RewriteRequestCase = ({ + rule_type_id: alertTypeId, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, + api_key_owner: apiKeyOwner, + notify_when: notifyWhen, + mute_all: muteAll, + muted_alert_ids: mutedInstanceIds, + scheduled_task_id: scheduledTaskId, + execution_status: executionStatus, + actions: actions, + ...rest +}: any) => ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined, + actions: actions + ? actions.map((action: AsApiContract) => transformAction(action)) + : [], + scheduledTaskId, + ...rest, +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts new file mode 100644 index 0000000000000..8d1ec57a4e63e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AlertUpdates } from '../../../types'; +import { createAlert } from './create'; + +const http = httpServiceMock.createStartContract(); + +describe('createAlert', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call create alert API', async () => { + const resolvedValue = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + rule_type_id: '.index-threshold', + notify_when: 'onActionGroupChange', + actions: [ + { + group: 'threshold met', + id: '1', + params: { + level: 'info', + message: 'alert ', + }, + connector_type_id: '.server-log', + }, + ], + scheduled_task_id: '1', + execution_status: { status: 'pending', last_execution_date: '2021-04-01T21:33:13.250Z' }, + create_at: '2021-04-01T21:33:13.247Z', + updated_at: '2021-04-01T21:33:13.247Z', + }; + const alertToCreate: Omit< + AlertUpdates, + 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' + > = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + enabled: true, + throttle: null, + alertTypeId: '.index-threshold', + notifyWhen: 'onActionGroupChange', + actions: [ + { + group: 'threshold met', + id: '83d4d860-9316-11eb-a145-93ab369a4461', + params: { + level: 'info', + message: + "alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}", + }, + actionTypeId: '.server-log', + }, + ], + createdAt: new Date('2021-04-01T21:33:13.247Z'), + updatedAt: new Date('2021-04-01T21:33:13.247Z'), + apiKeyOwner: '', + }; + http.post.mockResolvedValueOnce(resolvedValue); + + const result = await createAlert({ http, alert: alertToCreate }); + expect(result).toEqual({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + id: '1', + params: { + level: 'info', + message: 'alert ', + }, + }, + ], + alertTypeId: '.index-threshold', + apiKeyOwner: undefined, + consumer: 'alerts', + create_at: '2021-04-01T21:33:13.247Z', + createdAt: undefined, + createdBy: undefined, + executionStatus: { + lastExecutionDate: '2021-04-01T21:33:13.250Z', + status: 'pending', + }, + muteAll: undefined, + mutedInstanceIds: undefined, + name: 'test', + notifyWhen: 'onActionGroupChange', + params: { + aggType: 'count', + groupBy: 'all', + index: ['.kibana'], + termSize: 5, + threshold: [1000], + thresholdComparator: '>', + timeField: 'alert.executionStatus.lastExecutionDate', + timeWindowSize: 5, + timeWindowUnit: 'm', + }, + schedule: { + interval: '1m', + }, + scheduledTaskId: '1', + tags: [], + updatedAt: '2021-04-01T21:33:13.247Z', + updatedBy: undefined, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts new file mode 100644 index 0000000000000..bd92769b4bbf3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { RewriteResponseCase } from '../../../../../actions/common'; +import { Alert, AlertUpdates } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { transformAlert } from './common_transformations'; + +type AlertCreateBody = Omit< + AlertUpdates, + 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' +>; +const rewriteBodyRequest: RewriteResponseCase = ({ + alertTypeId, + notifyWhen, + actions, + ...res +}): any => ({ + ...res, + rule_type_id: alertTypeId, + notify_when: notifyWhen, + actions: actions.map(({ group, id, params }) => ({ + group, + id, + params, + })), +}); + +export async function createAlert({ + http, + alert, +}: { + http: HttpSetup; + alert: AlertCreateBody; +}): Promise { + const res = await http.post(`${BASE_ALERTING_API_PATH}/rule`, { + body: JSON.stringify(rewriteBodyRequest(alert)), + }); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts new file mode 100644 index 0000000000000..b279e4c0237d9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { deleteAlerts } from './delete'; + +const http = httpServiceMock.createStartContract(); + +describe('deleteAlerts', () => { + test('should call delete API for each alert', async () => { + const ids = ['1', '2', '3']; + const result = await deleteAlerts({ http, ids }); + expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); + expect(http.delete.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1", + ], + Array [ + "/api/alerting/rule/2", + ], + Array [ + "/api/alerting/rule/3", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts new file mode 100644 index 0000000000000..870d5a409c3dd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function deleteAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise<{ successes: string[]; errors: string[] }> { + const successes: string[] = []; + const errors: string[] = []; + await Promise.all(ids.map((id) => http.delete(`${BASE_ALERTING_API_PATH}/rule/${id}`))).then( + function (fulfilled) { + successes.push(...fulfilled); + }, + function (rejected) { + errors.push(...rejected); + } + ); + return { successes, errors }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts new file mode 100644 index 0000000000000..90d1cd13096e8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { disableAlert, disableAlerts } from './disable'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_disable", + ], + ] + `); + }); +}); + +describe('disableAlerts', () => { + test('should call disable alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await disableAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_disable", + ], + Array [ + "/api/alerting/rule/2/_disable", + ], + Array [ + "/api/alerting/rule/3/_disable", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts new file mode 100644 index 0000000000000..cc0939fbebfbd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_disable`); +} + +export async function disableAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => disableAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts new file mode 100644 index 0000000000000..ef65e8b605cba --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { enableAlert, enableAlerts } from './enable'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_enable", + ], + ] + `); + }); +}); + +describe('enableAlerts', () => { + test('should call enable alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await enableAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_enable", + ], + Array [ + "/api/alerting/rule/2/_enable", + ], + Array [ + "/api/alerting/rule/3/_enable", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts new file mode 100644 index 0000000000000..3c16ffaec6223 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_enable`); +} + +export async function enableAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => enableAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts new file mode 100644 index 0000000000000..f2d8337eb4091 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlert } from './get_rule'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: '1', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + rule_type_id: '.index-threshold', + created_by: 'elastic', + updated_by: 'elastic', + created_at: '2021-04-01T20:29:18.652Z', + updated_at: '2021-04-01T20:33:38.260Z', + api_key_owner: 'elastic', + notify_when: 'onThrottleInterval', + mute_all: false, + muted_alert_ids: [], + scheduled_task_id: '1', + execution_status: { status: 'ok', last_execution_date: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + connector_type_id: '.index', + }, + ], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual({ + id: '1', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + alertTypeId: '.index-threshold', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2021-04-01T20:29:18.652Z', + updatedAt: '2021-04-01T20:33:38.260Z', + apiKeyOwner: 'elastic', + notifyWhen: 'onThrottleInterval', + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '1', + executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + actionTypeId: '.index', + }, + ], + }); + expect(http.get).toHaveBeenCalledWith(`/api/alerting/rule/${alertId}`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts new file mode 100644 index 0000000000000..2e4cbc9b50c51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { Alert } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { transformAlert } from './common_transformations'; + +export async function loadAlert({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/rule/${alertId}`); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts new file mode 100644 index 0000000000000..e08306bee0f9c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { alertingFrameworkHealth } from './health'; + +describe('alertingFrameworkHealth', () => { + const http = httpServiceMock.createStartContract(); + test('should call alertingFrameworkHealth API', async () => { + http.get.mockResolvedValueOnce({ + is_sufficiently_secure: true, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, + }); + const result = await alertingFrameworkHealth({ http }); + expect(result).toEqual({ + alertingFrameworkHeath: { + decryptionHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + executionHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + readHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: true, + }); + expect(http.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/_health", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts new file mode 100644 index 0000000000000..9468f4b3c03e0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { AlertingFrameworkHealth, AlertsHealth } from '../../../../../alerting/common'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +const rewriteAlertingFrameworkHeath: RewriteRequestCase = ({ + decryption_health: decryptionHealth, + execution_health: executionHealth, + read_health: readHealth, + ...res +}: AsApiContract) => ({ + decryptionHealth, + executionHealth, + readHealth, + ...res, +}); + +const rewriteBodyRes: RewriteRequestCase = ({ + is_sufficiently_secure: isSufficientlySecure, + has_permanent_encryption_key: hasPermanentEncryptionKey, + alerting_framework_heath: alertingFrameworkHeath, + ...res +}: AsApiContract) => ({ + isSufficientlySecure, + hasPermanentEncryptionKey, + alertingFrameworkHeath, + ...res, +}); + +export async function alertingFrameworkHealth({ + http, +}: { + http: HttpSetup; +}): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/_health`); + const alertingFrameworkHeath = rewriteAlertingFrameworkHeath(res.alerting_framework_heath); + return { ...rewriteBodyRes(res), alertingFrameworkHeath }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts new file mode 100644 index 0000000000000..a0b090a474e28 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { alertingFrameworkHealth } from './health'; +export { mapFiltersToKql } from './map_filters_to_kql'; +export { loadAlertAggregations } from './aggregate'; +export { createAlert } from './create'; +export { deleteAlerts } from './delete'; +export { disableAlert, disableAlerts } from './disable'; +export { enableAlert, enableAlerts } from './enable'; +export { loadAlert } from './get_rule'; +export { loadAlertInstanceSummary } from './alert_summary'; +export { muteAlertInstance } from './mute_alert'; +export { muteAlert, muteAlerts } from './mute'; +export { loadAlertTypes } from './rule_types'; +export { loadAlerts } from './rules'; +export { loadAlertState } from './state'; +export { unmuteAlertInstance } from './unmute_alert'; +export { unmuteAlert, unmuteAlerts } from './unmute'; +export { updateAlert } from './update'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts new file mode 100644 index 0000000000000..4e5e2a412dad6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapFiltersToKql } from './map_filters_to_kql'; + +describe('mapFiltersToKql', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should handle no filters', () => { + expect(mapFiltersToKql({})).toEqual([]); + }); + + test('should handle typesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + }) + ).toEqual(['alert.attributes.alertTypeId:(type or filter)']); + }); + + test('should handle actionTypesFilter', () => { + expect( + mapFiltersToKql({ + actionTypesFilter: ['action', 'types', 'filter'], + }) + ).toEqual([ + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + ]); + }); + + test('should handle alertStatusesFilter', () => { + expect( + mapFiltersToKql({ + alertStatusesFilter: ['alert', 'statuses', 'filter'], + }) + ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); + }); + + test('should handle typesFilter and actionTypesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + actionTypesFilter: ['action', 'types', 'filter'], + }) + ).toEqual([ + 'alert.attributes.alertTypeId:(type or filter)', + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + ]); + }); + + test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + actionTypesFilter: ['action', 'types', 'filter'], + alertStatusesFilter: ['alert', 'statuses', 'filter'], + }) + ).toEqual([ + 'alert.attributes.alertTypeId:(type or filter)', + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + 'alert.attributes.executionStatus.status:(alert or statuses or filter)', + ]); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts new file mode 100644 index 0000000000000..4c30e960034bf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mapFiltersToKql = ({ + typesFilter, + actionTypesFilter, + alertStatusesFilter, +}: { + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; +}): string[] => { + const filters = []; + if (typesFilter && typesFilter.length) { + filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); + } + if (actionTypesFilter && actionTypesFilter.length) { + filters.push( + [ + '(', + actionTypesFilter + .map((id) => `alert.attributes.actions:{ actionTypeId:${id} }`) + .join(' OR '), + ')', + ].join('') + ); + } + if (alertStatusesFilter && alertStatusesFilter.length) { + filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); + } + return filters; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts new file mode 100644 index 0000000000000..75143dd6b7f85 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { muteAlert, muteAlerts } from './mute'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_mute_all", + ], + ] + `); + }); +}); + +describe('muteAlerts', () => { + test('should call mute alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await muteAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_mute_all", + ], + Array [ + "/api/alerting/rule/2/_mute_all", + ], + Array [ + "/api/alerting/rule/3/_mute_all", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts new file mode 100644 index 0000000000000..22a96d7a11ff3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_mute_all`); +} + +export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { + await Promise.all(ids.map((id) => muteAlert({ http, id }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts new file mode 100644 index 0000000000000..4365cce42c8c3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { muteAlertInstance } from './mute_alert'; + +const http = httpServiceMock.createStartContract(); + +describe('muteAlertInstance', () => { + test('should call mute instance alert API', async () => { + const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/alert/123/_mute", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts new file mode 100644 index 0000000000000..0bb05010cfa3c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function muteAlertInstance({ + id, + instanceId, + http, +}: { + id: string; + instanceId: string; + http: HttpSetup; +}): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_mute`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts new file mode 100644 index 0000000000000..71513ed0c6e61 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertType } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertTypes } from './rule_types'; +import { ALERTS_FEATURE_ID } from '../../../../../alerting/common'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertTypes', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertType[] = [ + { + id: 'test', + name: 'Test', + actionVariables: { + context: [{ name: 'var1', description: 'val1' }], + state: [{ name: 'var2', description: 'val2' }], + params: [{ name: 'var3', description: 'val3' }], + }, + producer: ALERTS_FEATURE_ID, + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }, + ]; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertTypes({ http }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rule_types", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts new file mode 100644 index 0000000000000..54369d7959c93 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AlertType } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteResponseRes = (results: Array>): AlertType[] => { + return results.map((item) => rewriteBodyReq(item)); +}; + +const rewriteBodyReq: RewriteRequestCase = ({ + enabled_in_license: enabledInLicense, + recovery_action_group: recoveryActionGroup, + action_groups: actionGroups, + default_action_group_id: defaultActionGroupId, + minimum_license_required: minimumLicenseRequired, + action_variables: actionVariables, + authorized_consumers: authorizedConsumers, + ...rest +}: AsApiContract) => ({ + enabledInLicense, + recoveryActionGroup, + actionGroups, + defaultActionGroupId, + minimumLicenseRequired, + actionVariables, + authorizedConsumers, + ...rest, +}); + +export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/rule_types`); + return rewriteResponseRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts new file mode 100644 index 0000000000000..602507c08066c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlerts } from './rules'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlerts', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call find API with base parameters', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with searchText', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with actionTypesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'foo', + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with actionTypesFilter and typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'baz', + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with searchText and tagsFilter and typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'apples, foo, baz', + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "apples, foo, baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts new file mode 100644 index 0000000000000..f0bbb57180bb4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { Alert, Pagination, Sorting } from '../../../types'; +import { AsApiContract } from '../../../../../actions/common'; +import { mapFiltersToKql } from './map_filters_to_kql'; +import { transformAlert } from './common_transformations'; + +const rewriteResponseRes = (results: Array>): Alert[] => { + return results.map((item) => transformAlert(item)); +}; + +export async function loadAlerts({ + http, + page, + searchText, + typesFilter, + actionTypesFilter, + alertStatusesFilter, + sort = { field: 'name', direction: 'asc' }, +}: { + http: HttpSetup; + page: Pagination; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; + sort?: Sorting; +}): Promise<{ + page: number; + perPage: number; + total: number; + data: Alert[]; +}> { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const res = await http.get(`${BASE_ALERTING_API_PATH}/rules/_find`, { + query: { + page: page.index + 1, + per_page: page.size, + search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, + search: searchText, + filter: filters.length ? filters.join(' and ') : undefined, + default_search_operator: 'AND', + sort_field: sort.field, + sort_order: sort.direction, + }, + }); + return { + page: res.page, + perPage: res.per_page, + total: res.total, + data: rewriteResponseRes(res.data), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts new file mode 100644 index 0000000000000..ae27352be0b90 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertState } from './state'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertState', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: {}, + second_instance: {}, + }, + }; + http.get.mockResolvedValueOnce({ + rule_type_state: { + some: 'value', + }, + alerts: { + first_instance: {}, + second_instance: {}, + }, + }); + + expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); + + test('should parse AlertInstances', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }; + http.get.mockResolvedValueOnce({ + rule_type_state: { + some: 'value', + }, + alerts: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }); + + expect(await loadAlertState({ http, alertId })).toEqual({ + ...resolvedValue, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date('2020-02-09T23:15:41.941Z'), + }, + }, + }, + }, + }); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); + + test('should handle empty response from api', async () => { + const alertId = uuid.v4(); + http.get.mockResolvedValueOnce(''); + + expect(await loadAlertState({ http, alertId })).toEqual({}); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts new file mode 100644 index 0000000000000..428bc5b99a70b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { Errors, identity } from 'io-ts'; +import { AlertTaskState } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { alertStateSchema } from '../../../../../alerting/common'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + rule_type_state: alertTypeState, + alerts: alertInstances, + previous_started_at: previousStartedAt, + ...rest +}: any) => ({ + ...rest, + alertTypeState, + alertInstances, + previousStartedAt, +}); + +type EmptyHttpResponse = ''; +export async function loadAlertState({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http + .get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/state`) + .then((state: AsApiContract | EmptyHttpResponse) => + state ? rewriteBodyRes(state) : {} + ) + .then((state: AlertTaskState) => { + return pipe( + alertStateSchema.decode(state), + fold((e: Errors) => { + throw new Error(`Alert "${alertId}" has invalid state`); + }, identity) + ); + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts new file mode 100644 index 0000000000000..68a6feeb65e1e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { unmuteAlert, unmuteAlerts } from './unmute'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('unmuteAlerts', () => { + test('should call unmute alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await unmuteAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_unmute_all", + ], + Array [ + "/api/alerting/rule/2/_unmute_all", + ], + Array [ + "/api/alerting/rule/3/_unmute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_unmute_all", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts new file mode 100644 index 0000000000000..c65be6a670a89 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_unmute_all`); +} + +export async function unmuteAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts new file mode 100644 index 0000000000000..c0131cbab0ebf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { unmuteAlertInstance } from './unmute_alert'; + +const http = httpServiceMock.createStartContract(); + +describe('unmuteAlertInstance', () => { + test('should call mute instance alert API', async () => { + const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/alert/123/_unmute", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts new file mode 100644 index 0000000000000..60d2cca72b85e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function unmuteAlertInstance({ + id, + instanceId, + http, +}: { + id: string; + instanceId: string; + http: HttpSetup; +}): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_unmute`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts new file mode 100644 index 0000000000000..745a94b8d1134 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Alert } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { updateAlert } from './update'; +import { AlertNotifyWhenType } from '../../../../../alerting/common'; + +const http = httpServiceMock.createStartContract(); + +describe('updateAlert', () => { + test('should call alert update API', async () => { + const alertToUpdate = { + throttle: '1m', + consumer: 'alerts', + name: 'test', + tags: ['foo'], + schedule: { + interval: '1m', + }, + params: {}, + actions: [], + createdAt: new Date('1970-01-01T00:00:00.000Z'), + updatedAt: new Date('1970-01-01T00:00:00.000Z'), + apiKey: null, + apiKeyOwner: null, + notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, + }; + const resolvedValue: Alert = { + ...alertToUpdate, + id: '123', + enabled: true, + alertTypeId: 'test', + createdBy: null, + updatedBy: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + }; + http.put.mockResolvedValueOnce(resolvedValue); + + const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); + expect(result).toEqual(resolvedValue); + expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rule/123", + Object { + "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"notify_when\\":\\"onThrottleInterval\\",\\"actions\\":[]}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts new file mode 100644 index 0000000000000..44b9306949f81 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { pick } from 'lodash'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { Alert, AlertUpdates } from '../../../types'; +import { RewriteResponseCase } from '../../../../../actions/common'; +import { transformAlert } from './common_transformations'; + +type AlertUpdatesBody = Pick< + AlertUpdates, + 'name' | 'tags' | 'schedule' | 'actions' | 'params' | 'throttle' | 'notifyWhen' +>; +const rewriteBodyRequest: RewriteResponseCase = ({ + notifyWhen, + actions, + ...res +}): any => ({ + ...res, + notify_when: notifyWhen, + actions: actions.map(({ group, id, params }) => ({ + group, + id, + params, + })), +}); + +export async function updateAlert({ + http, + alert, + id, +}: { + http: HttpSetup; + alert: Pick< + AlertUpdates, + 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' + >; + id: string; +}): Promise { + const res = await http.put(`${BASE_ALERTING_API_PATH}/rule/${id}`, { + body: JSON.stringify( + rewriteBodyRequest( + pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) + ) + ), + }); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index b7a63d7043f49..8202449b22298 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/ui_actions_enhanced/tsconfig.json b/x-pack/plugins/ui_actions_enhanced/tsconfig.json index 1513669cdc1ad..39318770126e5 100644 --- a/x-pack/plugins/ui_actions_enhanced/tsconfig.json +++ b/x-pack/plugins/ui_actions_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 08e45bebf125b..0d65c8ddd8fed 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index aea4ce947511e..4ba836c1e5d26 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,8 +1,16 @@ { - "configPath": ["xpack", "uptime"], + "configPath": [ + "xpack", + "uptime" + ], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["data", "home", "observability", "ml"], + "optionalPlugins": [ + "data", + "home", + "observability", + "ml" + ], "requiredPlugins": [ "alerting", "embeddable", @@ -17,9 +25,9 @@ "requiredBundles": [ "observability", "kibanaReact", + "kibanaUtils", "home", "data", - "ml", - "maps" + "ml" ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 7ea6b72547386..a578fced134e8 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -29,6 +29,7 @@ import { import { alertTypeInitializers } from '../lib/alert_types'; import { FetchDataParams, ObservabilityPublicSetup } from '../../../observability/public'; import { PLUGIN } from '../../common/constants/plugin'; +import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; export interface ClientPluginsSetup { data: DataPublicPluginSetup; @@ -43,6 +44,13 @@ export interface ClientPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } +export interface UptimePluginServices extends Partial { + embeddable: EmbeddableStart; + data: DataPublicPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + storage: IStorageWrapper; +} + export type ClientSetup = void; export type ClientStart = void; diff --git a/x-pack/plugins/uptime/public/apps/uptime_app.tsx b/x-pack/plugins/uptime/public/apps/uptime_app.tsx index a72572fe86f5a..8e049be75434d 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_app.tsx @@ -31,6 +31,7 @@ import { store } from '../state'; import { kibanaService } from '../state/kibana_service'; import { ActionMenu } from '../components/common/header/action_menu'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; export interface UptimeAppColors { danger: string; @@ -96,12 +97,20 @@ const Application = (props: UptimeAppProps) => { store.dispatch(setBasePath(basePath)); + const storage = new Storage(window.localStorage); + return ( diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx index 719c90574b088..5dfc11837b72d 100644 --- a/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { CertMonitors } from './cert_monitors'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; describe('CertMonitors', () => { const certMons = [ diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx index a0166fc573754..a991634de22a6 100644 --- a/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/cert_search.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; import { CertificateSearch } from './cert_search'; describe('CertificatesSearch', () => { diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx index 999d76f690867..e331a6e5c34fe 100644 --- a/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/cert_status.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; import { CertStatus } from './cert_status'; import * as redux from 'react-redux'; import moment from 'moment'; diff --git a/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx b/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx index 8ae0cdb791d9b..ec6a5d91a67c3 100644 --- a/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/certificates_list.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../lib'; import { CertificateList, CertSort } from './certificates_list'; describe('CertificateList', () => { diff --git a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx index 550b7f75623f0..1affd1f990f90 100644 --- a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; import { FingerprintCol } from './fingerprint_col'; import moment from 'moment'; diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx index 72b1145a9f34e..d7ae92a0e7654 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_charts.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import DateMath from '@elastic/datemath'; import { DurationChartComponent } from './duration_chart'; import { MonitorDurationResult } from '../../../../common/types'; -import { shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../../lib'; describe('MonitorCharts component', () => { let dateMathSpy: any; diff --git a/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx index b11595eafae4f..792b357b3baba 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { MonitorBarSeries, MonitorBarSeriesProps } from './monitor_bar_series'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; -import { MountWithReduxProvider } from '../../../lib/helper/helper_with_redux'; +import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib'; import { HistogramPoint } from '../../../../common/runtime_types'; describe('MonitorBarSeries component', () => { diff --git a/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx b/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx index bede71b8ba03d..6e04648a817f0 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_header.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import moment from 'moment'; import { PageHeader } from './page_header'; import { Ping } from '../../../../common/runtime_types'; -import { renderWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter } from '../../../lib'; import { mockReduxHooks } from '../../../lib/helper/test_helpers'; describe('PageHeader', () => { diff --git a/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx b/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx index 63465aefcdd43..fdb5498969d39 100644 --- a/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/monitor_tags.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MonitorTags } from './monitor_tags'; import * as hooks from '../../hooks/use_url_params'; -import { renderWithRouter, shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../lib'; describe('MonitorTags component', () => { const summaryPing = { diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx index d433e7fccd1b8..4bfe7de33cba5 100644 --- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx @@ -10,9 +10,9 @@ import { UptimeDatePicker } from './uptime_date_picker'; import { renderWithRouter, shallowWithRouter, + MountWithReduxProvider, mountWithRouterRedux, -} from '../../lib/helper/enzyme_helpers'; -import { MountWithReduxProvider } from '../../lib/helper/helper_with_redux'; +} from '../../lib'; import { UptimeStartupPluginsContextProvider } from '../../contexts'; import { startPlugins } from '../../lib/__mocks__/uptime_plugin_start_mock'; import { ClientPluginsStart } from '../../apps/plugin'; diff --git a/x-pack/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts index 73ac77a61461f..2c95ac3347723 100644 --- a/x-pack/plugins/uptime/public/components/monitor/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/index.ts @@ -7,7 +7,6 @@ export * from './ml'; export * from './ping_list'; -export * from './status_details/location_map'; export * from './status_details'; export * from './ping_histogram'; export * from './monitor_charts'; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx index 16d96148af340..f29be50633fab 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integerations.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { MLIntegrationComponent } from './ml_integeration'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; import * as redux from 'react-redux'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { coreMock } from 'src/core/public/mocks'; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx index 6bff0b61d7349..15a537a49ccf3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { coreMock } from 'src/core/public/mocks'; import { ManageMLJobComponent } from './manage_ml_job'; import * as redux from 'react-redux'; -import { renderWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; const core = coreMock.createStart(); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx index a1be391833bc3..3f107581c1eea 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import DateMath from '@elastic/datemath'; import { MonitorCharts } from './monitor_charts'; -import { shallowWithRouter } from '../../lib/helper/enzyme_helpers'; +import { shallowWithRouter } from '../../lib'; describe('MonitorCharts component', () => { let dateMathSpy: any; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx index 682be99b9b418..dabc0021898eb 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import * as reactRouterDom from 'react-router-dom'; import { Ping } from '../../../common/runtime_types'; import { MonitorPageTitle } from './monitor_title'; -import { renderWithRouter } from '../../lib/helper/enzyme_helpers'; +import { renderWithRouter } from '../../lib'; import { mockReduxHooks } from '../../lib/helper/test_helpers'; jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap deleted file mode 100644 index 94cbeb49a32cf..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap +++ /dev/null @@ -1,234 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationAvailability component doesnt shows warning if geo is provided 1`] = ` - - - - - - - - - - - - - -`; - -exports[`LocationAvailability component renders correctly against snapshot 1`] = ` - - - - -

    - Monitoring from -

    -
    -
    - - - -
    - - - - - -
    -`; - -exports[`LocationAvailability component renders named locations that have missing geo data 1`] = ` - - - - - - - - - - - - - - - -`; - -exports[`LocationAvailability component shows warning if geo information is missing 1`] = ` - - - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx index 2edb2eec46580..855b8ef0c9767 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx @@ -6,28 +6,16 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; +import { screen } from '@testing-library/react'; +import { render } from '../../../../lib/helper/rtl_helpers'; import { LocationAvailability } from './location_availability'; import { MonitorLocations } from '../../../../../common/runtime_types'; -import { LocationMissingWarning } from '../location_map/location_missing'; // Note For shallow test, we need absolute time strings describe('LocationAvailability component', () => { let monitorLocations: MonitorLocations; - let localStorageMock: any; - - let selectedView = 'list'; beforeEach(() => { - localStorageMock = { - getItem: jest.fn().mockImplementation(() => selectedView), - setItem: jest.fn(), - }; - - Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - }); - monitorLocations = { monitorId: 'wapo', up_history: 12, @@ -41,104 +29,34 @@ describe('LocationAvailability component', () => { down_history: 0, }, { - summary: { up: 4, down: 0 }, - geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, - }, - { - summary: { up: 4, down: 0 }, - geo: { name: 'Unnamed-location' }, - timestamp: '2020-01-13T22:50:02.753Z', - up_history: 4, - down_history: 0, - }, - ], - }; - }); - - it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('shows warning if geo information is missing', () => { - selectedView = 'map'; - monitorLocations = { - monitorId: 'wapo', - up_history: 8, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, + summary: { up: 2, down: 2 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, + up_history: 2, + down_history: 2, }, { - summary: { up: 4, down: 0 }, + summary: { up: 0, down: 4 }, geo: { name: 'Unnamed-location' }, timestamp: '2020-01-13T22:50:02.753Z', - up_history: 4, - down_history: 0, + up_history: 0, + down_history: 4, }, ], }; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - - const warningComponent = component.find(LocationMissingWarning); - expect(warningComponent).toHaveLength(1); }); - it('doesnt shows warning if geo is provided', () => { - monitorLocations = { - monitorId: 'wapo', - up_history: 8, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } }, - timestamp: '2020-01-13T22:50:06.536Z', - up_history: 4, - down_history: 0, - }, - { - summary: { up: 4, down: 0 }, - geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, - }, - ], - }; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - - const warningComponent = component.find(LocationMissingWarning); - expect(warningComponent).toHaveLength(0); - }); - - it('renders named locations that have missing geo data', () => { - monitorLocations = { - monitorId: 'wapo', - up_history: 4, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'New York', location: undefined }, - timestamp: '2020-01-13T22:50:06.536Z', - up_history: 4, - down_history: 0, - }, - ], - }; - - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); + it('renders correctly', () => { + render(); + expect(screen.getByRole('heading', { name: 'Monitoring from', level: 3 })); + expect(screen.getByText('New York')).toBeInTheDocument(); + expect(screen.getByText('Tokyo')).toBeInTheDocument(); + expect(screen.getByText('Unnamed-location')).toBeInTheDocument(); + expect(screen.getByText('100.00 %')).toBeInTheDocument(); + expect(screen.getByText('50.00 %')).toBeInTheDocument(); + expect(screen.getByText('0.00 %')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:06 PM')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:04 PM')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:02 PM')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx index 5f74098e12583..c851369d63e9e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx @@ -5,18 +5,12 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiTitle } from '@elastic/eui'; import { LocationStatusTags } from '../availability_reporting'; -import { LocationPoint } from '../location_map/embeddables/embedded_map'; -import { MonitorLocations, MonitorLocation } from '../../../../../common/runtime_types'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; -import { LocationMissingWarning } from '../location_map/location_missing'; -import { useSelectedView } from './use_selected_view'; -import { LocationMap } from '../location_map'; +import { MonitorLocations } from '../../../../../common/runtime_types'; import { MonitoringFrom } from '../translations'; -import { ToggleViewBtn } from './toggle_view_btn'; const EuiFlexItemTags = styled(EuiFlexItem)` width: 350px; @@ -30,61 +24,20 @@ interface LocationMapProps { } export const LocationAvailability = ({ monitorLocations }: LocationMapProps) => { - const upPoints: LocationPoint[] = []; - const downPoints: LocationPoint[] = []; - - let isAnyGeoInfoMissing = false; - - if (monitorLocations?.locations) { - monitorLocations.locations.forEach(({ geo, summary }: MonitorLocation) => { - if (geo?.name === UNNAMED_LOCATION || !geo?.location) { - isAnyGeoInfoMissing = true; - } else if (!!geo.location.lat && !!geo.location.lon) { - if (summary?.down === 0) { - upPoints.push(geo as LocationPoint); - } else { - downPoints.push(geo as LocationPoint); - } - } - }); - } - const { selectedView: initialView } = useSelectedView(); - - const [selectedView, setSelectedView] = useState(initialView); - return ( - {selectedView === 'list' && ( - - -

    {MonitoringFrom}

    -
    -
    - )} - {selectedView === 'map' && ( - {isAnyGeoInfoMissing && } - )} - - { - setSelectedView(val); - }} - /> + + +

    {MonitoringFrom}

    +
    - {selectedView === 'list' && ( - - - - )} - {selectedView === 'map' && ( - - - - )} + + +
    ); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx deleted file mode 100644 index 45cb5c45bf021..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import styled from 'styled-components'; -import { EuiButtonGroup } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSelectedView } from './use_selected_view'; -import { ChangeToListView, ChangeToMapView } from '../translations'; - -const ToggleViewButtons = styled.span` - margin-left: auto; -`; - -interface Props { - onChange: (val: string) => void; -} - -export const ToggleViewBtn = ({ onChange }: Props) => { - const toggleButtons = [ - { - id: `listBtn`, - label: ChangeToMapView, - name: 'listView', - iconType: 'list', - 'data-test-subj': 'uptimeMonitorToggleListBtn', - 'aria-label': ChangeToMapView, - }, - { - id: `mapBtn`, - label: ChangeToListView, - name: 'mapView', - iconType: 'mapMarker', - 'data-test-subj': 'uptimeMonitorToggleMapBtn', - 'aria-label': ChangeToListView, - }, - ]; - - const { selectedView, setSelectedView } = useSelectedView(); - - const onChangeView = (optionId: string) => { - const currView = optionId === 'listBtn' ? 'list' : 'map'; - setSelectedView(currView); - onChange(currView); - }; - - return ( - - onChangeView(id)} - type="multi" - isIconOnly - style={{ marginLeft: 'auto' }} - legend={i18n.translate('xpack.uptime.locationAvailabilityViewToggleLegend', { - defaultMessage: 'View toggle', - })} - /> - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts deleted file mode 100644 index fa77d0bf9057e..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts +++ /dev/null @@ -1,27 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; - -const localKey = 'xpack.uptime.detailPage.selectedView'; - -interface Props { - selectedView: string; - setSelectedView: (val: string) => void; -} - -export const useSelectedView = (): Props => { - const getSelectedView = localStorage.getItem(localKey) ?? 'list'; - - const [selectedView, setSelectedView] = useState(getSelectedView); - - useEffect(() => { - localStorage.setItem(localKey, selectedView); - }, [selectedView]); - - return { selectedView, setSelectedView }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap deleted file mode 100644 index 6b3d157c23fee..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMap component renders correctly against snapshot 1`] = ` - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap deleted file mode 100644 index 5e3e2e1a6db46..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap +++ /dev/null @@ -1,123 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMissingWarning component renders correctly against snapshot 1`] = ` -.c0 { - margin-left: auto; - margin-bottom: 3px; - margin-right: 5px; -} - -
    -
    -
    -
    - -
    -
    -
    -
    -`; - -exports[`LocationMissingWarning component shallow render correctly against snapshot 1`] = ` - - - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - observer.geo.?? - , - } - } - /> - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts deleted file mode 100644 index b925697970a57..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts +++ /dev/null @@ -1,192 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import lowPolyLayerFeatures from '../low_poly_layer.json'; - -export const mockDownPointsLayer = { - id: 'down_points', - label: 'Down Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features: [ - { - id: 'Asia', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 52.487239], - }, - }, - { - id: 'APJ', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 55.487239], - }, - }, - { - id: 'Canada', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [14.399262, 54.487239], - }, - }, - ], - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#BC261E', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', -}; - -export const mockUpPointsLayer = { - id: 'up_points', - label: 'Up Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features: [ - { - id: 'US-EAST', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 52.487239], - }, - }, - { - id: 'US-WEST', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 55.487239], - }, - }, - { - id: 'Europe', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [14.399262, 54.487239], - }, - }, - ], - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', -}; - -export const mockLayerList = [ - { - id: 'low_poly_layer', - label: 'World countries', - minZoom: 0, - maxZoom: 24, - alpha: 1, - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', - type: 'GEOJSON_FILE', - __featureCollection: lowPolyLayerFeatures, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#cad3e4', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 0, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }, - mockDownPointsLayer, - mockUpPointsLayer, -]; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx deleted file mode 100644 index 6706a435c7b6b..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx +++ /dev/null @@ -1,174 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState, useContext, useRef } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -import { - MapEmbeddable, - MapEmbeddableInput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../maps/public/embeddable'; -import * as i18n from './translations'; -import { GeoPoint } from '../../../../../../common/runtime_types'; -import { getLayerList } from './map_config'; -import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../../contexts'; -import { - isErrorEmbeddable, - ViewMode, - ErrorEmbeddable, -} from '../../../../../../../../../src/plugins/embeddable/public'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; -import { MapToolTipComponent } from './map_tool_tip'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; - -export interface EmbeddedMapProps { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export type LocationPoint = Required; - -const EmbeddedPanel = styled.div` - z-index: auto; - flex: 1; - display: flex; - flex-direction: column; - height: 100%; - position: relative; - .embPanel__content { - display: flex; - flex: 1 1 100%; - z-index: 1; - min-height: 0; // Absolute must for Firefox to scroll contents - } - &&& .mapboxgl-canvas { - animation: none !important; - } -`; - -export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { - const { colors } = useContext(UptimeThemeContext); - const [embeddable, setEmbeddable] = useState(); - const embeddableRoot: React.RefObject = useRef(null); - const { embeddable: embeddablePlugin } = useContext(UptimeStartupPluginsContext); - if (!embeddablePlugin) { - throw new Error('Embeddable start plugin not found'); - } - const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); - - const portalNode = React.useMemo(() => createPortalNode(), []); - - const input: MapEmbeddableInput = { - id: uuid.v4(), - attributes: { title: '' }, - filters: [], - hidePanelTitles: true, - refreshConfig: { - value: 0, - pause: false, - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - hideFilterActions: true, - // Zoom Lat/Lon values are set to make sure map is in center in the panel - // It wil also omit Greenland/Antarctica etc - mapCenter: { - lon: 11, - lat: 20, - zoom: 0, - }, - mapSettings: { - disableInteractive: true, - hideToolbarOverlay: true, - hideLayerControl: true, - hideViewControl: true, - }, - }; - - const renderTooltipContent = ({ - addFilters, - closeTooltip, - features, - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }: RenderTooltipContentParams) => { - const props = { - addFilters, - closeTooltip, - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }; - const relevantFeatures = features.filter( - (item: any) => item.layerId === 'up_points' || item.layerId === 'down_points' - ); - if (relevantFeatures.length > 0) { - return ; - } - closeTooltip(); - return null; - }; - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - throw new Error('Map embeddable not found.'); - } - const embeddableObject: any = await factory.create({ - ...input, - title: i18n.MAP_TITLE, - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - embeddableObject.setRenderTooltipContent(renderTooltipContent); - embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - - setEmbeddable(embeddableObject); - } - - setupEmbeddable(); - - // we want this effect to execute exactly once after the component mounts - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // update map layers based on points - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable)) { - embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - }, [upPoints, downPoints, embeddable, colors]); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot]); - - return ( - -
    - - - - - ); -}); - -EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json deleted file mode 100644 index 7a309cd01ebc7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json +++ /dev/null @@ -1,2898 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "34.21666", - "31.32333" - ], - [ - "35.98361", - "34.52750" - ], - [ - "34.65943", - "36.80527" - ], - [ - "32.77166", - "36.02888" - ], - [ - "29.67722", - "36.11833" - ], - [ - "27.25500", - "36.96500" - ], - [ - "27.51166", - "40.30555" - ], - [ - "33.33860", - "42.01985" - ], - [ - "38.35582", - "40.91027" - ], - [ - "41.77609", - "41.84193" - ], - [ - "41.59748", - "43.22151" - ], - [ - "45.16512", - "42.70333" - ], - [ - "47.91547", - "41.22499" - ], - [ - "49.76062", - "42.71076" - ], - [ - "49.44831", - "45.53038" - ], - [ - "47.30249", - "50.03194" - ], - [ - "52.34180", - "51.78075" - ], - [ - "55.69249", - "50.53249" - ], - [ - "58.33777", - "51.15610" - ], - [ - "57.97027", - "54.38819" - ], - [ - "59.64166", - "55.55867" - ], - [ - "57.22169", - "56.85096" - ], - [ - "59.44912", - "58.48804" - ], - [ - "59.57756", - "63.93287" - ], - [ - "66.10887", - "67.48123" - ], - [ - "64.52222", - "68.90305" - ], - [ - "67.05498", - "68.85637" - ], - [ - "69.32735", - "72.94540" - ], - [ - "73.52553", - "71.81582" - ], - [ - "80.82610", - "72.08693" - ], - [ - "80.51860", - "73.57346" - ], - [ - "89.25278", - "75.50305" - ], - [ - "97.18359", - "75.92804" - ], - [ - "104.07138", - "77.73221" - ], - [ - "111.10387", - "76.75526" - ], - [ - "113.47054", - "73.50096" - ], - [ - "118.63443", - "73.57166" - ], - [ - "131.53580", - "70.87776" - ], - [ - "137.45190", - "71.34109" - ], - [ - "141.02414", - "72.58582" - ], - [ - "149.18524", - "72.22249" - ], - [ - "152.53830", - "70.83777" - ], - [ - "159.72968", - "69.83472" - ], - [ - "170.61194", - "68.75633" - ], - [ - "170.47189", - "70.13416" - ], - [ - "180.00000", - "68.98010" - ], - [ - "180.00000", - "65.06891" - ], - [ - "179.55373", - "62.61971" - ], - [ - "173.54178", - "61.74430" - ], - [ - "170.64194", - "60.41750" - ], - [ - "163.36023", - "59.82388" - ], - [ - "161.93858", - "58.06763" - ], - [ - "163.34996", - "56.19596" - ], - [ - "156.74524", - "51.07791" - ], - [ - "155.54413", - "55.30360" - ], - [ - "155.94206", - "56.65353" - ], - [ - "161.91248", - "60.41972" - ], - [ - "159.24747", - "61.92222" - ], - [ - "152.35718", - "59.02332" - ], - [ - "143.21109", - "59.37666" - ], - [ - "137.72580", - "56.17500" - ], - [ - "137.29327", - "54.07500" - ], - [ - "141.41483", - "53.29361" - ], - [ - "140.17609", - "48.45013" - ], - [ - "135.42233", - "43.75611" - ], - [ - "133.15485", - "42.68263" - ], - [ - "131.81052", - "43.32555" - ], - [ - "129.70204", - "40.83069" - ], - [ - "127.51763", - "39.73957" - ], - [ - "129.42944", - "37.05986" - ], - [ - "129.23749", - "35.18990" - ], - [ - "126.37556", - "34.79138" - ], - [ - "126.38860", - "37.88721" - ], - [ - "124.32395", - "39.91589" - ], - [ - "121.64804", - "38.99638" - ], - [ - "121.17747", - "40.92194" - ], - [ - "118.11053", - "38.14639" - ], - [ - "120.82054", - "36.64527" - ], - [ - "120.24873", - "34.31145" - ], - [ - "121.84693", - "30.85305" - ], - [ - "120.93526", - "27.98222" - ], - [ - "119.58074", - "25.67996" - ], - [ - "116.48172", - "22.93902" - ], - [ - "112.28194", - "21.70139" - ], - [ - "107.36693", - "21.26527" - ], - [ - "105.63857", - "18.89065" - ], - [ - "108.82916", - "15.42194" - ], - [ - "109.46186", - "12.86097" - ], - [ - "109.02168", - "11.36225" - ], - [ - "104.79893", - "8.79222" - ], - [ - "104.98177", - "10.10444" - ], - [ - "100.97635", - "13.46281" - ], - [ - "99.15082", - "10.36472" - ], - [ - "100.57809", - "7.22014" - ], - [ - "103.18192", - "5.28278" - ], - [ - "103.37455", - "1.53347" - ], - [ - "101.28574", - "2.84354" - ], - [ - "100.35553", - "5.96389" - ], - [ - "98.27415", - "8.27444" - ], - [ - "98.74720", - "11.67486" - ], - [ - "97.72457", - "15.84666" - ], - [ - "95.42859", - "15.72972" - ], - [ - "93.72436", - "19.93243" - ], - [ - "91.70444", - "22.48055" - ], - [ - "86.96332", - "21.38194" - ], - [ - "86.42123", - "19.98493" - ], - [ - "80.27943", - "15.69917" - ], - [ - "79.85811", - "10.28583" - ], - [ - "76.99860", - "8.36527" - ], - [ - "74.85526", - "12.75500" - ], - [ - "73.44748", - "16.05861" - ], - [ - "72.56485", - "21.37506" - ], - [ - "70.82513", - "20.69597" - ], - [ - "66.50005", - "25.40381" - ], - [ - "61.76083", - "25.03208" - ], - [ - "57.31909", - "25.77146" - ], - [ - "56.80888", - "27.12361" - ], - [ - "54.78846", - "26.49041" - ], - [ - "51.43027", - "27.93777" - ], - [ - "50.63916", - "29.47042" - ], - [ - "47.95943", - "30.03305" - ], - [ - "48.83887", - "27.61972" - ], - [ - "51.28236", - "24.30000" - ], - [ - "53.58777", - "24.04417" - ], - [ - "55.85944", - "25.72042" - ], - [ - "57.17131", - "23.93444" - ], - [ - "59.82861", - "22.29166" - ], - [ - "57.80569", - "18.97097" - ], - [ - "55.03194", - "17.01472" - ], - [ - "52.18916", - "15.60528" - ], - [ - "45.04232", - "12.75239" - ], - [ - "43.47888", - "12.67500" - ], - [ - "42.78933", - "16.46083" - ], - [ - "40.75694", - "19.76417" - ], - [ - "39.17486", - "21.10402" - ], - [ - "39.06277", - "22.58333" - ], - [ - "35.16055", - "28.05666" - ], - [ - "34.21666", - "31.32333" - ] - ] - ], - [ - [ - [ - "-169.69496", - "66.06806" - ], - [ - "-173.67308", - "64.34679" - ], - [ - "-179.32083", - "65.53012" - ], - [ - "-180.00000", - "65.06891" - ], - [ - "-180.00000", - "68.98010" - ], - [ - "-169.69496", - "66.06806" - ] - ] - ], - [ - [ - [ - "139.93851", - "40.42860" - ], - [ - "142.06970", - "39.54666" - ], - [ - "140.95358", - "38.14805" - ], - [ - "140.33218", - "35.12985" - ], - [ - "137.02879", - "34.56784" - ], - [ - "136.71246", - "36.75139" - ], - [ - "139.42622", - "38.15458" - ], - [ - "139.93851", - "40.42860" - ] - ] - ], - [ - [ - [ - "119.89259", - "15.80112" - ], - [ - "120.58527", - "18.51139" - ], - [ - "122.51833", - "17.04389" - ], - [ - "121.38026", - "15.30250" - ], - [ - "119.89259", - "15.80112" - ] - ] - ], - [ - [ - [ - "122.32916", - "7.30833" - ], - [ - "126.18610", - "9.24277" - ], - [ - "125.37762", - "6.72361" - ], - [ - "123.45888", - "7.81055" - ], - [ - "122.32916", - "7.30833" - ] - ] - ], - [ - [ - [ - "111.89638", - "-3.57389" - ], - [ - "110.23193", - "-2.97111" - ], - [ - "108.84549", - "0.81056" - ], - [ - "109.64857", - "2.07341" - ], - [ - "113.01054", - "3.16055" - ], - [ - "115.37886", - "4.91167" - ], - [ - "116.75417", - "7.01805" - ], - [ - "119.27582", - "5.34500" - ], - [ - "117.27540", - "3.22000" - ], - [ - "117.87192", - "1.87667" - ], - [ - "117.44479", - "-0.52397" - ], - [ - "115.96624", - "-3.60875" - ], - [ - "113.03471", - "-2.98972" - ], - [ - "111.89638", - "-3.57389" - ] - ] - ], - [ - [ - [ - "102.97601", - "0.64348" - ], - [ - "103.36081", - "-0.70222" - ], - [ - "106.05525", - "-3.03139" - ], - [ - "105.72887", - "-5.89826" - ], - [ - "102.32610", - "-4.00611" - ], - [ - "100.90555", - "-2.31944" - ], - [ - "98.70383", - "1.55979" - ], - [ - "95.53108", - "4.68278" - ], - [ - "97.51483", - "5.24944" - ], - [ - "100.41219", - "2.29306" - ], - [ - "102.97601", - "0.64348" - ] - ] - ], - [ - [ - [ - "120.82723", - "1.23406" - ], - [ - "120.01999", - "-0.07528" - ], - [ - "122.47623", - "-3.16090" - ], - [ - "120.32888", - "-5.51208" - ], - [ - "119.35491", - "-5.40007" - ], - [ - "118.88860", - "-2.89319" - ], - [ - "119.77805", - "0.22972" - ], - [ - "120.82723", - "1.23406" - ] - ] - ], - [ - [ - [ - "136.04913", - "-2.69806" - ], - [ - "137.87579", - "-1.47306" - ], - [ - "144.51373", - "-3.82222" - ], - [ - "145.76639", - "-5.48528" - ], - [ - "147.46661", - "-5.97086" - ], - [ - "146.08969", - "-8.09111" - ], - [ - "144.21738", - "-7.79465" - ], - [ - "143.36510", - "-9.01222" - ], - [ - "141.11996", - "-9.23097" - ], - [ - "139.09454", - "-7.56181" - ], - [ - "138.06525", - "-5.40896" - ], - [ - "135.20468", - "-4.45972" - ], - [ - "132.72275", - "-2.81722" - ], - [ - "131.25555", - "-0.82278" - ], - [ - "134.02950", - "-0.96694" - ], - [ - "134.99495", - "-3.33653" - ], - [ - "136.04913", - "-2.69806" - ] - ] - ], - [ - [ - [ - "110.05640", - "-7.89751" - ], - [ - "106.56721", - "-7.41694" - ], - [ - "106.07582", - "-5.88194" - ], - [ - "110.39360", - "-6.97903" - ], - [ - "110.05640", - "-7.89751" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Asia" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "-25.28167", - "71.39166" - ], - [ - "-23.56056", - "70.10609" - ], - [ - "-26.36333", - "68.66748" - ], - [ - "-31.99916", - "68.09526" - ], - [ - "-34.71999", - "66.33832" - ], - [ - "-41.15541", - "64.96235" - ], - [ - "-43.08722", - "60.10027" - ], - [ - "-47.68986", - "61.00680" - ], - [ - "-50.31562", - "62.49430" - ], - [ - "-53.23333", - "65.68283" - ], - [ - "-53.62778", - "67.81470" - ], - [ - "-50.58930", - "69.92373" - ], - [ - "-54.68694", - "72.36721" - ], - [ - "-58.15958", - "75.50860" - ], - [ - "-68.50056", - "76.08693" - ], - [ - "-72.55222", - "78.52110" - ], - [ - "-60.80666", - "81.87997" - ], - [ - "-30.38833", - "83.60220" - ], - [ - "-16.00500", - "80.72859" - ], - [ - "-22.03695", - "77.68568" - ], - [ - "-19.33681", - "75.40207" - ], - [ - "-24.46305", - "73.53581" - ], - [ - "-25.28167", - "71.39166" - ] - ] - ], - [ - [ - [ - "-87.64890", - "76.33804" - ], - [ - "-86.47916", - "79.76167" - ], - [ - "-90.43666", - "81.88750" - ], - [ - "-70.26001", - "83.11388" - ], - [ - "-61.07639", - "82.32083" - ], - [ - "-78.78194", - "76.57221" - ], - [ - "-87.64890", - "76.33804" - ] - ] - ], - [ - [ - [ - "-123.83389", - "73.70027" - ], - [ - "-115.31903", - "73.47707" - ], - [ - "-123.29306", - "71.14610" - ], - [ - "-123.83389", - "73.70027" - ] - ] - ], - [ - [ - [ - "-65.32806", - "62.66610" - ], - [ - "-68.61583", - "62.26389" - ], - [ - "-77.33667", - "65.17609" - ], - [ - "-72.25835", - "67.24803" - ], - [ - "-77.30506", - "69.83395" - ], - [ - "-85.87465", - "70.07943" - ], - [ - "-89.90348", - "71.35304" - ], - [ - "-89.03958", - "73.25499" - ], - [ - "-81.57251", - "73.71971" - ], - [ - "-67.21986", - "69.94081" - ], - [ - "-67.23819", - "68.35790" - ], - [ - "-61.26458", - "66.62609" - ], - [ - "-65.56204", - "64.73154" - ], - [ - "-65.32806", - "62.66610" - ] - ] - ], - [ - [ - [ - "-105.02444", - "72.21999" - ], - [ - "-100.99973", - "70.17276" - ], - [ - "-101.85139", - "68.98442" - ], - [ - "-113.04173", - "68.49374" - ], - [ - "-116.53221", - "69.40887" - ], - [ - "-119.13445", - "71.77457" - ], - [ - "-114.66666", - "73.37247" - ], - [ - "-105.02444", - "72.21999" - ] - ] - ], - [ - [ - [ - "-77.36667", - "8.67500" - ], - [ - "-77.88972", - "7.22889" - ], - [ - "-79.69778", - "8.86666" - ], - [ - "-81.73862", - "8.16250" - ], - [ - "-85.65668", - "9.90500" - ], - [ - "-85.66959", - "11.05500" - ], - [ - "-87.93779", - "13.15639" - ], - [ - "-91.38474", - "13.97889" - ], - [ - "-93.93861", - "16.09389" - ], - [ - "-96.47612", - "15.64361" - ], - [ - "-103.45001", - "18.31361" - ], - [ - "-105.67834", - "20.38305" - ], - [ - "-105.18945", - "21.43750" - ], - [ - "-106.91570", - "23.86514" - ], - [ - "-109.43750", - "25.82027" - ], - [ - "-109.44431", - "26.71555" - ], - [ - "-112.16195", - "28.97139" - ], - [ - "-113.09167", - "31.22972" - ], - [ - "-115.69667", - "29.77423" - ], - [ - "-117.40944", - "33.24416" - ], - [ - "-120.60583", - "34.55860" - ], - [ - "-124.33118", - "40.27246" - ], - [ - "-124.52444", - "42.86610" - ], - [ - "-123.87161", - "45.52898" - ], - [ - "-124.71431", - "48.39708" - ], - [ - "-124.03510", - "49.91801" - ], - [ - "-127.17315", - "50.92221" - ], - [ - "-130.88640", - "55.70791" - ], - [ - "-133.81302", - "57.97293" - ], - [ - "-136.65891", - "58.21652" - ], - [ - "-140.40335", - "59.69804" - ], - [ - "-146.75543", - "60.95249" - ], - [ - "-154.23567", - "58.13069" - ], - [ - "-157.55139", - "58.38777" - ], - [ - "-165.42244", - "60.55215" - ], - [ - "-164.40112", - "63.21499" - ], - [ - "-168.13196", - "65.66296" - ], - [ - "-161.66779", - "67.02054" - ], - [ - "-166.82362", - "68.34873" - ], - [ - "-156.59673", - "71.35144" - ], - [ - "-151.22986", - "70.37296" - ], - [ - "-143.21555", - "70.11026" - ], - [ - "-137.25500", - "68.94832" - ], - [ - "-127.18096", - "70.27638" - ], - [ - "-114.06652", - "68.46970" - ], - [ - "-112.39584", - "67.67915" - ], - [ - "-98.11124", - "67.83887" - ], - [ - "-90.43639", - "68.87442" - ], - [ - "-85.55499", - "69.85970" - ], - [ - "-81.33570", - "69.18498" - ], - [ - "-81.50222", - "67.00096" - ], - [ - "-85.89726", - "66.16802" - ], - [ - "-87.98736", - "64.18845" - ], - [ - "-92.71001", - "62.46583" - ], - [ - "-94.78972", - "59.09222" - ], - [ - "-92.41875", - "57.33270" - ], - [ - "-88.81500", - "56.82444" - ], - [ - "-85.00195", - "55.29666" - ], - [ - "-82.30777", - "55.14888" - ], - [ - "-82.27390", - "52.95638" - ], - [ - "-78.57945", - "52.11138" - ], - [ - "-79.76181", - "54.65166" - ], - [ - "-76.67979", - "56.03645" - ], - [ - "-78.57299", - "58.62888" - ], - [ - "-77.50835", - "62.56166" - ], - [ - "-73.68346", - "62.47999" - ], - [ - "-70.14848", - "61.08458" - ], - [ - "-67.56610", - "58.22360" - ], - [ - "-64.74538", - "60.23075" - ], - [ - "-61.09055", - "55.84415" - ], - [ - "-57.34969", - "54.57496" - ], - [ - "-56.95160", - "51.42458" - ], - [ - "-60.00500", - "50.24888" - ], - [ - "-66.44903", - "50.26777" - ], - [ - "-64.21167", - "48.88499" - ], - [ - "-64.90430", - "46.84597" - ], - [ - "-63.66708", - "45.81666" - ], - [ - "-70.19187", - "43.57555" - ], - [ - "-70.72610", - "41.72777" - ], - [ - "-74.13390", - "40.70082" - ], - [ - "-75.96083", - "37.15221" - ], - [ - "-76.34326", - "34.88194" - ], - [ - "-78.82750", - "33.73027" - ], - [ - "-81.48843", - "31.11347" - ], - [ - "-80.03534", - "26.79569" - ], - [ - "-81.73659", - "25.95944" - ], - [ - "-84.01098", - "30.09764" - ], - [ - "-88.98083", - "30.41833" - ], - [ - "-94.75417", - "29.36791" - ], - [ - "-97.56041", - "26.84208" - ], - [ - "-97.74223", - "22.01250" - ], - [ - "-95.80112", - "18.74500" - ], - [ - "-94.46918", - "18.14625" - ], - [ - "-90.73167", - "19.36153" - ], - [ - "-90.27972", - "21.06305" - ], - [ - "-86.82973", - "21.42923" - ], - [ - "-88.28250", - "17.62389" - ], - [ - "-88.13696", - "15.68285" - ], - [ - "-84.26015", - "15.82597" - ], - [ - "-83.18695", - "14.32389" - ], - [ - "-83.84751", - "11.17458" - ], - [ - "-82.24278", - "9.00236" - ], - [ - "-79.53445", - "9.62014" - ], - [ - "-77.36667", - "8.67500" - ] - ] - ], - [ - [ - [ - "-55.19333", - "46.98499" - ], - [ - "-59.40361", - "47.89423" - ], - [ - "-56.68250", - "51.33943" - ], - [ - "-55.56114", - "49.36818" - ], - [ - "-52.83465", - "48.09965" - ], - [ - "-55.19333", - "46.98499" - ] - ] - ], - [ - [ - [ - "-73.03644", - "18.45622" - ], - [ - "-72.79834", - "19.94278" - ], - [ - "-69.94932", - "19.67680" - ], - [ - "-68.89528", - "18.39639" - ], - [ - "-73.03644", - "18.45622" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "North America" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "64.52222", - "68.90305" - ], - [ - "66.10887", - "67.48123" - ], - [ - "59.57756", - "63.93287" - ], - [ - "59.44912", - "58.48804" - ], - [ - "57.22169", - "56.85096" - ], - [ - "59.64166", - "55.55867" - ], - [ - "57.97027", - "54.38819" - ], - [ - "58.33777", - "51.15610" - ], - [ - "55.69249", - "50.53249" - ], - [ - "52.34180", - "51.78075" - ], - [ - "47.30249", - "50.03194" - ], - [ - "49.44831", - "45.53038" - ], - [ - "49.76062", - "42.71076" - ], - [ - "47.91547", - "41.22499" - ], - [ - "45.16512", - "42.70333" - ], - [ - "41.59748", - "43.22151" - ], - [ - "39.94553", - "43.39693" - ], - [ - "34.70249", - "46.17582" - ], - [ - "30.83277", - "46.54832" - ], - [ - "28.78083", - "44.66096" - ], - [ - "28.01305", - "41.98222" - ], - [ - "26.36041", - "40.95388" - ], - [ - "22.59500", - "40.01221" - ], - [ - "23.96055", - "38.28166" - ], - [ - "22.15246", - "37.01854" - ], - [ - "19.30721", - "40.64531" - ], - [ - "19.59771", - "41.80611" - ], - [ - "15.15167", - "44.19639" - ], - [ - "13.02958", - "41.26014" - ], - [ - "8.74722", - "44.42805" - ], - [ - "6.16528", - "43.05055" - ], - [ - "4.05625", - "43.56277" - ], - [ - "3.20167", - "41.89278" - ], - [ - "0.99306", - "41.04805" - ], - [ - "0.20722", - "38.73221" - ], - [ - "-2.12292", - "36.73347" - ], - [ - "-5.61361", - "36.00610" - ], - [ - "-6.95992", - "37.22184" - ], - [ - "-8.98924", - "37.02631" - ], - [ - "-9.49083", - "38.79388" - ], - [ - "-8.66014", - "40.69111" - ], - [ - "-9.16972", - "43.18583" - ], - [ - "-1.44389", - "43.64055" - ], - [ - "-1.11463", - "46.31658" - ], - [ - "-2.68528", - "48.50166" - ], - [ - "1.43875", - "50.10083" - ], - [ - "5.59917", - "53.30028" - ], - [ - "13.80854", - "53.85479" - ], - [ - "21.24506", - "54.95506" - ], - [ - "21.05223", - "56.81749" - ], - [ - "23.43159", - "59.95382" - ], - [ - "21.42416", - "60.57930" - ], - [ - "21.58500", - "64.43971" - ], - [ - "17.09861", - "61.60278" - ], - [ - "19.07264", - "59.73819" - ], - [ - "16.37982", - "56.66333" - ], - [ - "12.46007", - "56.29666" - ], - [ - "10.51569", - "59.30624" - ], - [ - "8.12750", - "58.09888" - ], - [ - "5.50847", - "58.66764" - ], - [ - "4.94944", - "61.41041" - ], - [ - "9.54528", - "63.76611" - ], - [ - "15.28833", - "68.03055" - ], - [ - "21.30000", - "70.24693" - ], - [ - "28.20778", - "71.07999" - ], - [ - "32.80605", - "69.30277" - ], - [ - "43.75180", - "67.31152" - ], - [ - "53.60437", - "68.90818" - ], - [ - "64.52222", - "68.90305" - ] - ] - ], - [ - [ - [ - "-13.49944", - "65.06915" - ], - [ - "-18.77500", - "63.39139" - ], - [ - "-22.04556", - "64.04666" - ], - [ - "-22.42167", - "66.43332" - ], - [ - "-16.41736", - "66.27603" - ], - [ - "-13.49944", - "65.06915" - ] - ] - ], - [ - [ - [ - "-4.19667", - "57.48583" - ], - [ - "-0.07931", - "54.11340" - ], - [ - "0.25389", - "50.73861" - ], - [ - "-3.43722", - "50.60500" - ], - [ - "-4.19639", - "53.20611" - ], - [ - "-2.89979", - "53.72499" - ], - [ - "-6.22778", - "56.69722" - ], - [ - "-4.19667", - "57.48583" - ] - ] - ], - [ - [ - [ - "12.44167", - "37.80611" - ], - [ - "15.64794", - "38.26458" - ], - [ - "15.08139", - "36.64916" - ], - [ - "12.44167", - "37.80611" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Europe" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "34.21666", - "31.32333" - ], - [ - "34.90380", - "29.48671" - ], - [ - "33.93833", - "26.65528" - ], - [ - "36.88625", - "22.05319" - ], - [ - "37.43569", - "18.85389" - ], - [ - "38.58902", - "18.06680" - ], - [ - "39.71805", - "15.08805" - ], - [ - "41.17222", - "14.63069" - ], - [ - "43.32750", - "12.47673" - ], - [ - "44.27833", - "10.44778" - ], - [ - "50.09319", - "11.51458" - ], - [ - "51.14555", - "10.63361" - ], - [ - "48.00055", - "4.52306" - ], - [ - "46.02555", - "2.43722" - ], - [ - "43.48861", - "0.65000" - ], - [ - "40.12548", - "-3.26569" - ], - [ - "38.77611", - "-6.03972" - ], - [ - "40.38777", - "-11.31778" - ], - [ - "40.57833", - "-15.49889" - ], - [ - "34.89069", - "-19.86042" - ], - [ - "35.45611", - "-24.16945" - ], - [ - "32.81111", - "-25.61209" - ], - [ - "32.39444", - "-28.53139" - ], - [ - "27.90000", - "-33.04056" - ], - [ - "24.82472", - "-34.20167" - ], - [ - "22.53916", - "-34.01118" - ], - [ - "20.00000", - "-34.82200" - ], - [ - "17.84750", - "-32.83083" - ], - [ - "18.21791", - "-31.73458" - ], - [ - "15.09500", - "-26.73528" - ], - [ - "14.51139", - "-22.55278" - ], - [ - "11.76764", - "-17.98820" - ], - [ - "11.73125", - "-15.85070" - ], - [ - "13.84944", - "-10.95611" - ], - [ - "13.39180", - "-8.39375" - ], - [ - "11.77417", - "-4.54264" - ], - [ - "9.70250", - "-2.44792" - ], - [ - "9.29833", - "-0.37167" - ], - [ - "9.96514", - "3.08521" - ], - [ - "8.89861", - "4.58833" - ], - [ - "5.93583", - "4.33833" - ], - [ - "4.41021", - "6.35993" - ], - [ - "1.46889", - "6.18639" - ], - [ - "-2.05889", - "4.73083" - ], - [ - "-4.46806", - "5.29556" - ], - [ - "-7.43639", - "4.34917" - ], - [ - "-9.23889", - "5.12278" - ], - [ - "-12.50417", - "7.38861" - ], - [ - "-13.49313", - "9.56008" - ], - [ - "-15.00542", - "10.77194" - ], - [ - "-17.17556", - "14.65444" - ], - [ - "-16.03945", - "17.73458" - ], - [ - "-16.91625", - "21.94542" - ], - [ - "-12.96271", - "27.92048" - ], - [ - "-11.51195", - "28.30375" - ], - [ - "-9.64097", - "30.16500" - ], - [ - "-8.53833", - "33.25055" - ], - [ - "-6.84306", - "34.01861" - ], - [ - "-5.91874", - "35.79065" - ], - [ - "-1.97972", - "35.07333" - ], - [ - "1.18250", - "36.51221" - ], - [ - "9.85868", - "37.32833" - ], - [ - "11.12667", - "35.24194" - ], - [ - "11.17430", - "33.21006" - ], - [ - "15.16583", - "32.39861" - ], - [ - "15.75430", - "31.38972" - ], - [ - "18.95750", - "30.27639" - ], - [ - "20.56763", - "32.56091" - ], - [ - "29.03500", - "30.82417" - ], - [ - "30.35545", - "31.50284" - ], - [ - "34.21666", - "31.32333" - ] - ] - ], - [ - [ - [ - "48.03140", - "-14.06341" - ], - [ - "49.94333", - "-13.03945" - ], - [ - "50.48277", - "-15.40583" - ], - [ - "49.36833", - "-18.35139" - ], - [ - "47.13305", - "-24.92806" - ], - [ - "44.01708", - "-24.98083" - ], - [ - "43.23888", - "-22.28250" - ], - [ - "44.48277", - "-19.96584" - ], - [ - "43.93139", - "-17.50056" - ], - [ - "44.87360", - "-16.21028" - ], - [ - "48.03140", - "-14.06341" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Africa" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - "-77.88972", - "7.22889" - ], - [ - "-77.36667", - "8.67500" - ], - [ - "-75.63432", - "9.44819" - ], - [ - "-74.86081", - "11.12549" - ], - [ - "-68.84368", - "11.44708" - ], - [ - "-68.11424", - "10.48493" - ], - [ - "-61.87959", - "10.72833" - ], - [ - "-61.61987", - "9.90528" - ], - [ - "-57.51919", - "6.27077" - ], - [ - "-52.97320", - "5.47305" - ], - [ - "-51.25931", - "4.15250" - ], - [ - "-49.90320", - "1.17444" - ], - [ - "-51.92751", - "-1.33486" - ], - [ - "-48.42722", - "-1.66028" - ], - [ - "-47.28556", - "-0.59917" - ], - [ - "-42.23584", - "-2.83778" - ], - [ - "-39.99875", - "-2.84653" - ], - [ - "-37.17445", - "-4.91861" - ], - [ - "-35.47973", - "-5.16611" - ], - [ - "-34.83129", - "-6.98180" - ], - [ - "-35.32751", - "-9.22889" - ], - [ - "-39.05709", - "-13.38028" - ], - [ - "-38.87195", - "-15.87417" - ], - [ - "-39.70403", - "-19.42361" - ], - [ - "-42.03445", - "-22.91917" - ], - [ - "-44.67521", - "-23.05570" - ], - [ - "-48.02612", - "-25.01500" - ], - [ - "-48.84251", - "-28.61778" - ], - [ - "-52.21764", - "-31.74500" - ], - [ - "-54.14077", - "-34.66466" - ], - [ - "-56.15834", - "-34.92722" - ], - [ - "-56.67834", - "-36.92361" - ], - [ - "-58.30112", - "-38.48500" - ], - [ - "-62.06875", - "-39.50848" - ], - [ - "-62.39001", - "-40.90195" - ], - [ - "-65.13014", - "-40.84417" - ], - [ - "-65.24945", - "-44.31306" - ], - [ - "-67.58435", - "-46.00030" - ], - [ - "-65.78979", - "-47.96584" - ], - [ - "-68.94112", - "-50.38806" - ], - [ - "-68.99014", - "-51.62445" - ], - [ - "-72.11501", - "-53.68764" - ], - [ - "-74.28924", - "-50.48049" - ], - [ - "-74.74139", - "-47.71146" - ], - [ - "-72.61389", - "-44.47278" - ], - [ - "-73.99432", - "-40.96695" - ], - [ - "-73.22404", - "-39.41688" - ], - [ - "-73.67709", - "-37.34729" - ], - [ - "-71.44667", - "-32.66500" - ], - [ - "-71.69585", - "-30.50667" - ], - [ - "-70.91389", - "-27.62445" - ], - [ - "-70.05334", - "-21.42565" - ], - [ - "-70.31202", - "-18.43750" - ], - [ - "-71.49424", - "-17.30223" - ], - [ - "-75.05139", - "-15.46597" - ], - [ - "-76.39480", - "-13.88417" - ], - [ - "-78.99459", - "-8.21965" - ], - [ - "-81.17473", - "-6.08667" - ], - [ - "-81.27640", - "-4.28083" - ], - [ - "-79.95632", - "-3.20778" - ], - [ - "-80.91279", - "-1.03653" - ], - [ - "-80.10084", - "0.77028" - ], - [ - "-78.88929", - "1.23837" - ], - [ - "-77.43445", - "4.03139" - ], - [ - "-77.88972", - "7.22889" - ] - ] - ] - }, - "properties": { - "CONTINENT": "South America" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "177.91779", - "-38.94280" - ], - [ - "175.95523", - "-41.25528" - ], - [ - "173.75165", - "-39.27000" - ], - [ - "174.94025", - "-38.10111" - ], - [ - "177.91779", - "-38.94280" - ] - ] - ], - [ - [ - [ - "171.18524", - "-44.93833" - ], - [ - "169.45801", - "-46.62333" - ], - [ - "166.47690", - "-45.80972" - ], - [ - "168.37233", - "-44.04056" - ], - [ - "171.15166", - "-42.56042" - ], - [ - "172.63025", - "-40.51056" - ], - [ - "174.23636", - "-41.83722" - ], - [ - "171.18524", - "-44.93833" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Oceania" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - "151.54025", - "-24.04583" - ], - [ - "153.18192", - "-25.94944" - ], - [ - "153.62419", - "-28.66104" - ], - [ - "152.52969", - "-32.40361" - ], - [ - "151.45456", - "-33.31681" - ], - [ - "149.97163", - "-37.52222" - ], - [ - "146.87357", - "-38.65166" - ], - [ - "143.54295", - "-38.85923" - ], - [ - "140.52997", - "-38.00028" - ], - [ - "138.09225", - "-34.13493" - ], - [ - "135.49586", - "-34.61708" - ], - [ - "134.18414", - "-32.48666" - ], - [ - "131.14859", - "-31.47403" - ], - [ - "125.97227", - "-32.26674" - ], - [ - "123.73499", - "-33.77972" - ], - [ - "120.00499", - "-33.92889" - ], - [ - "117.93414", - "-35.12534" - ], - [ - "115.00895", - "-34.26243" - ], - [ - "115.73998", - "-31.86806" - ], - [ - "113.64346", - "-26.65431" - ], - [ - "113.38971", - "-24.42944" - ], - [ - "114.03027", - "-21.84167" - ], - [ - "116.70749", - "-20.64917" - ], - [ - "121.02748", - "-19.59222" - ], - [ - "122.95623", - "-16.58681" - ], - [ - "126.85790", - "-13.75097" - ], - [ - "129.08942", - "-14.89944" - ], - [ - "130.57927", - "-12.40465" - ], - [ - "132.67198", - "-11.50813" - ], - [ - "135.23135", - "-12.29445" - ], - [ - "135.45135", - "-14.93278" - ], - [ - "136.76581", - "-15.90445" - ], - [ - "140.83330", - "-17.45194" - ], - [ - "141.66553", - "-15.02653" - ], - [ - "141.59412", - "-12.53167" - ], - [ - "142.78830", - "-11.08056" - ], - [ - "143.78220", - "-14.41333" - ], - [ - "145.31580", - "-14.94555" - ], - [ - "146.27762", - "-18.88701" - ], - [ - "147.43192", - "-19.41236" - ], - [ - "150.81912", - "-22.73194" - ], - [ - "151.54025", - "-24.04583" - ] - ] - ] - }, - "properties": { - "CONTINENT": "Australia" - } - } - ] -} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts deleted file mode 100644 index 5ad92d4e6d1d7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getLayerList } from './map_config'; -import { mockLayerList } from './__mocks__/poly_layer_mock'; -import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../apps/uptime_app'; - -jest.mock('uuid', () => { - return { - v4: jest.fn(() => 'uuid.v4()'), - }; -}); - -describe('map_config', () => { - let upPoints: LocationPoint[]; - let downPoints: LocationPoint[]; - let colors: Pick; - - beforeEach(() => { - upPoints = [ - { name: 'US-EAST', location: { lat: '52.487239', lon: '13.399262' } }, - { location: { lat: '55.487239', lon: '13.399262' }, name: 'US-WEST' }, - { location: { lat: '54.487239', lon: '14.399262' }, name: 'Europe' }, - ]; - downPoints = [ - { location: { lat: '52.487239', lon: '13.399262' }, name: 'Asia' }, - { location: { lat: '55.487239', lon: '13.399262' }, name: 'APJ' }, - { location: { lat: '54.487239', lon: '14.399262' }, name: 'Canada' }, - ]; - colors = { - danger: '#BC261E', - gray: '#000', - }; - }); - - describe('#getLayerList', () => { - test('it returns the low poly layer', () => { - const layerList = getLayerList(upPoints, downPoints, colors); - expect(layerList).toStrictEqual(mockLayerList); - }); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts deleted file mode 100644 index 723eee6f14b80..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts +++ /dev/null @@ -1,175 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import lowPolyLayerFeatures from './low_poly_layer.json'; -import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../apps/uptime_app'; - -/** - * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, - * destination, and line layer for each of the provided indexPatterns - * - */ -export const getLayerList = ( - upPoints: LocationPoint[], - downPoints: LocationPoint[], - { danger }: Pick -) => { - return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; -}; - -export const getLowPolyLayer = () => { - return { - id: 'low_poly_layer', - label: 'World countries', - minZoom: 0, - maxZoom: 24, - alpha: 1, - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', - type: 'GEOJSON_FILE', - __featureCollection: lowPolyLayerFeatures, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#cad3e4', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 0, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: string) => { - const features = downPoints?.map((point) => ({ - type: 'feature', - id: point.name, - geometry: { - type: 'Point', - coordinates: [+point.location.lon, +point.location.lat], - }, - })); - return { - id: 'down_points', - label: 'Down Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features, - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: dangerColor, - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -export const getUpPointsLayer = (upPoints: LocationPoint[]) => { - const features = upPoints?.map((point) => ({ - type: 'feature', - id: point.name, - geometry: { - type: 'Point', - coordinates: [+point.location.lon, +point.location.lat], - }, - })); - return { - id: 'up_points', - label: 'Up Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features, - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx deleted file mode 100644 index c03ed94f8c544..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ /dev/null @@ -1,91 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; -import { useSelector } from 'react-redux'; -import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiOutsideClickDetector, - EuiPopoverTitle, -} from '@elastic/eui'; -import { TagLabel } from '../../availability_reporting'; -import { UptimeThemeContext } from '../../../../../contexts'; -import { AppState } from '../../../../../state'; -import { monitorLocationsSelector } from '../../../../../state/selectors'; -import { useMonitorId } from '../../../../../hooks'; -import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; -import type { RenderTooltipContentParams } from '../../../../../../../maps/public'; -import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; -import { LastCheckLabel } from '../../translations'; - -type MapToolTipProps = Partial; - -export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipProps) => { - const { id: featureId, layerId } = features[0] ?? {}; - const locationName = featureId?.toString(); - const { - colors: { gray, danger }, - } = useContext(UptimeThemeContext); - - const monitorId = useMonitorId(); - - const monitorLocations = useSelector((state: AppState) => - monitorLocationsSelector(state, monitorId) - ); - if (!locationName || !monitorLocations?.locations) { - return null; - } - const { - timestamp, - up_history: ups, - down_history: downs, - }: MonitorLocation = monitorLocations.locations!.find( - ({ geo }: MonitorLocation) => geo.name === locationName - )!; - - const availability = (ups / (ups + downs)) * 100; - - return ( - { - if (closeTooltip != null) { - closeTooltip(); - } - }} - > - <> - - {layerId === 'up_points' ? ( - - ) : ( - - )} - - - Availability - - {i18n.translate('xpack.uptime.mapToolTip.AvailabilityStat.title', { - defaultMessage: '{value} %', - values: { value: formatAvailabilityValue(availability) }, - description: 'A percentage value like 23.5%', - })} - - {LastCheckLabel} - - {moment(timestamp).fromNow()} - - - - - ); -}; - -export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts deleted file mode 100644 index edbf2b5b5e864..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const MAP_TITLE = i18n.translate( - 'xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle', - { - defaultMessage: 'Monitor Observer Location Map', - } -); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx deleted file mode 100644 index 9818fc164193c..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { LocationMap } from './location_map'; -import { LocationPoint } from './embeddables/embedded_map'; - -// Note For shallow test, we need absolute time strings -describe('LocationMap component', () => { - let upPoints: LocationPoint[]; - - beforeEach(() => { - upPoints = [ - { - name: 'New York', - location: { lat: '40.730610', lon: ' -73.935242' }, - }, - { - name: 'Tokyo', - location: { lat: '52.487448', lon: ' 13.394798' }, - }, - ]; - }); - - it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx deleted file mode 100644 index 5a912a44b7c9a..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; - -// These height/width values are used to make sure map is in center of panel -// And to make sure, it doesn't take too much space -const MapPanel = styled.div` - height: 240px; - width: 520px; - margin-right: 65px; - @media (max-width: 574px) { - height: 250px; - width: 100%; - } -`; - -interface Props { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export const LocationMap = ({ upPoints, downPoints }: Props) => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx deleted file mode 100644 index dad7a61e74999..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { LocationMissingWarning } from './location_missing'; - -describe('LocationMissingWarning component', () => { - it('shallow render correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('renders correctly against snapshot', () => { - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx deleted file mode 100644 index 7b03f516decad..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx +++ /dev/null @@ -1,79 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiSpacer, - EuiText, - EuiCode, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { LocationLink } from '../../../common/location_link'; - -const EuiPopoverRight = styled(EuiFlexItem)` - margin-left: auto; - margin-bottom: 3px; - margin-right: 5px; -`; - -export const LocationMissingWarning = () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const togglePopover = () => { - setIsPopoverOpen(!isPopoverOpen); - }; - - const button = ( - - - - ); - - return ( - - - - - observer.geo.?? }} - /> - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx index ba0853b5b1b60..af3c47b9caf30 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/monitor_status.bar.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MonitorStatusBar } from './status_bar'; import { Ping } from '../../../../common/runtime_types'; import * as redux from 'react-redux'; -import { renderWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { renderWithRouter } from '../../../lib'; import { createMemoryHistory } from 'history'; describe('MonitorStatusBar component', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx index 0cb7ff7168404..03ce292e63621 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/ssl_certificate.test.tsx @@ -11,11 +11,7 @@ import { EuiIcon } from '@elastic/eui'; import { Tls } from '../../../../common/runtime_types'; import { MonitorSSLCertificate } from './status_bar'; import * as redux from 'react-redux'; -import { - mountWithRouter, - renderWithRouter, - shallowWithRouter, -} from '../../../lib/helper/enzyme_helpers'; +import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../lib'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('SSL Certificate component', () => { diff --git a/x-pack/plugins/uptime/public/components/overview/__snapshots__/parsing_error_callout.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__snapshots__/parsing_error_callout.test.tsx.snap deleted file mode 100644 index 653b739145f30..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/__snapshots__/parsing_error_callout.test.tsx.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OverviewPageParsingErrorCallout renders without errors when a valid error is provided 1`] = ` - -

    - - Unable to convert to Elasticsearch query, invalid syntax. - , - } - } - /> -

    -
    -`; - -exports[`OverviewPageParsingErrorCallout renders without errors when an error with no message is provided 1`] = ` - -

    - - There was no error message - , - } - } - /> -

    -
    -`; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx new file mode 100644 index 0000000000000..488d3221ae489 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { QueryStringInput } from '../../../../../../../../src/plugins/data/public'; +import { useIndexPattern } from '../../query_bar/use_index_pattern'; +import { isValidKuery } from '../../query_bar/query_bar'; +import * as labels from '../translations'; +import { useGetUrlParams } from '../../../../hooks'; + +interface Props { + query: string; + onChange: (query: string) => void; +} + +export const AlertQueryBar = ({ query, onChange }: Props) => { + const { index_pattern: indexPattern } = useIndexPattern(); + + const { search } = useGetUrlParams(); + + const [inputVal, setInputVal] = useState(search ?? ''); + + useEffect(() => { + onChange(search); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + { + setInputVal(queryN?.query as string); + if (isValidKuery(queryN?.query as string)) { + // we want to submit when user clears or paste a complete kuery + onChange(queryN.query as string); + } + }} + onSubmit={(queryN) => { + if (queryN) onChange(queryN.query as string); + }} + query={{ query: inputVal, language: 'kuery' }} + aria-label={labels.ALERT_KUERY_BAR_ARIA} + data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar" + autoSubmit={true} + disableLanguageSwitcher={true} + isInvalid={!!(inputVal && !query)} + placeholder={i18n.translate('xpack.uptime.alerts.searchPlaceholder.kql', { + defaultMessage: 'Filter using kql syntax', + })} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx index ab5f1f7bf8e18..d55f3fb336a9d 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx @@ -5,30 +5,18 @@ * 2.0. */ -import React, { useMemo, useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { isRight } from 'fp-ts/lib/Either'; -import { - selectMonitorStatusAlert, - overviewFiltersSelector, - snapshotDataSelector, - esKuerySelector, - selectedFiltersSelector, -} from '../../../../state/selectors'; -import { AlertMonitorStatusComponent } from '../index'; -import { - fetchOverviewFilters, - setSearchTextAction, - setEsKueryString, - getSnapshotCountAction, -} from '../../../../state/actions'; +import { overviewFiltersSelector, selectedFiltersSelector } from '../../../../state/selectors'; +import { AlertMonitorStatusComponent } from '../monitor_status_alert/alert_monitor_status'; +import { fetchOverviewFilters, setSearchTextAction } from '../../../../state/actions'; import { AtomicStatusCheckParamsType, GetMonitorAvailabilityParamsType, } from '../../../../../common/runtime_types'; -import { useIndexPattern } from '../../kuery_bar/use_index_pattern'; -import { useUpdateKueryString } from '../../../../hooks'; + +import { useSnapShotCount } from './use_snap_shot'; interface Props { alertParams: { [key: string]: any }; @@ -63,27 +51,17 @@ export const AlertMonitorStatus: React.FC = ({ }, [alertParams, dispatch]); const overviewFilters = useSelector(overviewFiltersSelector); - const { locations } = useSelector(selectMonitorStatusAlert); + useEffect(() => { if (alertParams.search) { dispatch(setSearchTextAction(alertParams.search)); } }, [alertParams, dispatch]); - const { index_pattern: indexPattern } = useIndexPattern(); - - const { count, loading } = useSelector(snapshotDataSelector); - const esKuery = useSelector(esKuerySelector); - const [esFilters] = useUpdateKueryString( - indexPattern, - alertParams.search, - alertParams.filters === undefined || typeof alertParams.filters === 'string' - ? '' - : JSON.stringify(Array.from(Object.entries(alertParams.filters))) - ); - useEffect(() => { - dispatch(setEsKueryString(esFilters ?? '')); - }, [dispatch, esFilters]); + const { count, loading } = useSnapShotCount({ + query: alertParams.search, + filters: alertParams.filters, + }); const isOldAlert = React.useMemo( () => @@ -92,15 +70,6 @@ export const AlertMonitorStatus: React.FC = ({ !isRight(GetMonitorAvailabilityParamsType.decode(alertParams)), [alertParams] ); - useEffect(() => { - dispatch( - getSnapshotCountAction.get({ - dateRangeStart: 'now-24h', - dateRangeEnd: 'now', - filters: esKuery, - }) - ); - }, [dispatch, esKuery]); const selectedFilters = useSelector(selectedFiltersSelector); useEffect(() => { @@ -118,19 +87,14 @@ export const AlertMonitorStatus: React.FC = ({ } }, [alertParams, setAlertParams, selectedFilters]); - const { pathname } = useLocation(); - const shouldUpdateUrl = useMemo(() => pathname.indexOf('app/uptime') !== -1, [pathname]); - return ( { + const parsedFilters = + filters === undefined || typeof filters === 'string' + ? '' + : JSON.stringify(Array.from(Object.entries(filters))); + + const { index_pattern: indexPattern } = useIndexPattern(); + + const [esKuery, error] = useUpdateKueryString(indexPattern, query, parsedFilters); + + const { data, loading } = useFetcher( + () => + fetchSnapshotCount({ + dateRangeStart: 'now-24h', + dateRangeEnd: 'now', + filters: error ? undefined : esKuery, + }), + [esKuery, query] + ); + + return { count: data || { total: 0, up: 0, down: 0 }, loading }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/index.ts index 46fe25b785879..85c2392044254 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/index.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { AlertMonitorStatusComponent } from './alert_monitor_status'; +export { AlertMonitorStatusComponent } from './monitor_status_alert/alert_monitor_status'; export { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button'; export { UptimeAlertsFlyoutWrapperComponent } from './uptime_alerts_flyout_wrapper'; export * from './alerts_containers'; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.test.tsx index dab7c5fbf1336..c0bf73d6c5308 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.test.tsx @@ -32,17 +32,10 @@ describe('FiltersExpressionSelect', () => { tags: [], }} setAlertParams={jest.fn()} - setUpdatedFieldValues={jest.fn()} shouldUpdateUrl={false} /> ); - expect(component).toMatchInlineSnapshot(` - - - - `); + expect(component).toMatchInlineSnapshot(``); }); it.each([ @@ -71,7 +64,6 @@ describe('FiltersExpressionSelect', () => { locations: [], }} setAlertParams={jest.fn()} - setUpdatedFieldValues={jest.fn()} shouldUpdateUrl={false} /> ); @@ -99,7 +91,6 @@ describe('FiltersExpressionSelect', () => { locations: ['nyc'], }} setAlertParams={setAlertParamsMock} - setUpdatedFieldValues={jest.fn()} shouldUpdateUrl={false} /> ); @@ -194,7 +185,6 @@ describe('FiltersExpressionSelect', () => { onRemoveFilter={jest.fn()} filters={filters} setAlertParams={jest.fn()} - setUpdatedFieldValues={jest.fn()} shouldUpdateUrl={false} /> ); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx index b0783f0528f56..b09d44488e803 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx @@ -13,13 +13,7 @@ import { alertFilterLabels, filterAriaLabels } from './translations'; import { FilterExpressionsSelectProps } from './filters_expression_select_container'; import { OverviewFiltersState } from '../../../../state/reducers/overview_filters'; -type FilterFieldUpdate = (updateTarget: { fieldName: string; values: string[] }) => void; - -interface OwnProps { - setUpdatedFieldValues: FilterFieldUpdate; -} - -type Props = FilterExpressionsSelectProps & Pick & OwnProps; +type Props = FilterExpressionsSelectProps & Pick; export const FiltersExpressionsSelect: React.FC = ({ alertParams, @@ -27,13 +21,15 @@ export const FiltersExpressionsSelect: React.FC = ({ newFilters, onRemoveFilter, setAlertParams, - setUpdatedFieldValues, }) => { const { tags, ports, schemes, locations } = overviewFilters; - const selectedPorts = alertParams?.filters?.['url.port'] ?? []; - const selectedLocations = alertParams?.filters?.['observer.geo.name'] ?? []; - const selectedSchemes = alertParams?.filters?.['monitor.type'] ?? []; - const selectedTags = alertParams?.filters?.tags ?? []; + + const alertFilters = alertParams?.filters; + + const selectedPorts = alertFilters?.['url.port'] ?? []; + const selectedLocations = alertFilters?.['observer.geo.name'] ?? []; + const selectedSchemes = alertFilters?.['monitor.type'] ?? []; + const selectedTags = alertFilters?.tags ?? []; const onFilterFieldChange = (fieldName: string, values: string[]) => { // the `filters` field is no longer a string @@ -54,7 +50,6 @@ export const FiltersExpressionsSelect: React.FC = ({ ) ); } - setUpdatedFieldValues({ fieldName, values }); }; const monitorFilters = [ @@ -162,12 +157,9 @@ export const FiltersExpressionsSelect: React.FC = ({ }} /> - ))} - - ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select_container.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select_container.tsx index 54567fee2cc77..0c03d55ba38f5 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select_container.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { FiltersExpressionsSelect } from './filters_expression_select'; import { overviewFiltersSelector } from '../../../../state/selectors'; -import { useFilterUpdate } from '../../../../hooks/use_filter_update'; export interface FilterExpressionsSelectProps { alertParams: { [key: string]: any }; @@ -20,20 +19,7 @@ export interface FilterExpressionsSelectProps { } export const FiltersExpressionSelectContainer: React.FC = (props) => { - const [updatedFieldValues, setUpdatedFieldValues] = useState<{ - fieldName: string; - values: string[]; - }>({ fieldName: '', values: [] }); - - useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values, props.shouldUpdateUrl); - const overviewFilters = useSelector(overviewFiltersSelector); - return ( - - ); + return ; }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.test.tsx similarity index 97% rename from x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.test.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.test.tsx index 5688b104217e2..86d6f599fd22d 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.test.tsx @@ -22,8 +22,10 @@ describe('AddFilterButton component', () => { Add filter @@ -86,8 +88,10 @@ describe('AddFilterButton component', () => { Add filter @@ -137,8 +141,10 @@ describe('AddFilterButton component', () => { Add filter diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx similarity index 96% rename from x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx index a2c13b68d6beb..66f0f296b1248 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; -import * as labels from './translations'; +import * as labels from '../translations'; interface Props { newFilters: string[]; @@ -60,6 +60,8 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler disabled={items.length === 0} iconType="plusInCircleFilled" onClick={onButtonClick} + size="s" + flush="left" > {labels.ADD_FILTER} diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx similarity index 79% rename from x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.test.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx index 6e8bf64c5bf02..274fb99ca47f9 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx @@ -21,8 +21,6 @@ describe('alert monitor status component', () => { enabled: true, hasFilters: false, isOldAlert: true, - locations: [], - shouldUpdateUrl: false, snapshotCount: 0, snapshotLoading: false, numTimes: 14, @@ -37,15 +35,30 @@ describe('alert monitor status component', () => { + + + + + } + /> - { isOldAlert={true} setAlertParams={[MockFunction]} /> - - - } - /> diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx similarity index 69% rename from x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx index 2c3a8fb284036..a20cb46454f26 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx @@ -6,26 +6,23 @@ */ import React, { useState } from 'react'; -import { EuiCallOut, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import * as labels from './translations'; -import { FiltersExpressionSelectContainer, StatusExpressionSelect } from './monitor_expressions'; +import { FiltersExpressionSelectContainer, StatusExpressionSelect } from '../monitor_expressions'; import { AddFilterButton } from './add_filter_btn'; import { OldAlertCallOut } from './old_alert_call_out'; -import { AvailabilityExpressionSelect } from './monitor_expressions/availability_expression_select'; -import { KueryBar } from '..'; +import { AvailabilityExpressionSelect } from '../monitor_expressions/availability_expression_select'; +import { AlertQueryBar } from '../alert_query_bar/query_bar'; export interface AlertMonitorStatusProps { alertParams: { [key: string]: any }; enabled: boolean; hasFilters: boolean; isOldAlert: boolean; - locations: string[]; snapshotCount: number; - snapshotLoading: boolean; + snapshotLoading?: boolean; numTimes: number; setAlertParams: (key: string, value: any) => void; - shouldUpdateUrl: boolean; timerange: { from: string; to: string; @@ -38,7 +35,6 @@ export const AlertMonitorStatusComponent: React.FC = (p hasFilters, isOldAlert, setAlertParams, - shouldUpdateUrl, snapshotCount, snapshotLoading, } = props; @@ -52,14 +48,26 @@ export const AlertMonitorStatusComponent: React.FC = (p <> - + + {' '} + {snapshotLoading && } + + } + iconType="iInCircle" + /> + + - setAlertParams('search', value)} - data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar" + setAlertParams('search', value)} /> @@ -81,7 +89,7 @@ export const AlertMonitorStatusComponent: React.FC = (p } }} setAlertParams={setAlertParams} - shouldUpdateUrl={shouldUpdateUrl} + shouldUpdateUrl={false} /> @@ -100,20 +108,6 @@ export const AlertMonitorStatusComponent: React.FC = (p setAlertParams={setAlertParams} /> - - - - } - iconType="iInCircle" - /> - ); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/old_alert_call_out.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/old_alert_call_out.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/overview/alerts/old_alert_call_out.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/old_alert_call_out.tsx diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/old_alert_callout.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/old_alert_callout.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/overview/alerts/old_alert_callout.test.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/old_alert_callout.test.tsx diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx index c751e6a0c24fa..a617ba0db1eb3 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EmptyStateComponent } from './empty_state'; import { StatesIndexStatus } from '../../../../common/runtime_types'; import { HttpFetchError, IHttpFetchError } from 'src/core/public'; -import { mountWithRouter, shallowWithRouter } from '../../../lib/helper/enzyme_helpers'; +import { mountWithRouter, shallowWithRouter } from '../../../lib'; describe('EmptyState component', () => { let statesIndexStatus: StatesIndexStatus; diff --git a/x-pack/plugins/uptime/public/components/overview/index.ts b/x-pack/plugins/uptime/public/components/overview/index.ts index 65e44d7dd4da4..729db44aaa964 100644 --- a/x-pack/plugins/uptime/public/components/overview/index.ts +++ b/x-pack/plugins/uptime/public/components/overview/index.ts @@ -10,6 +10,3 @@ export * from './empty_state'; export * from './filter_group'; export * from './alerts'; export * from './snapshot'; -export * from './kuery_bar'; - -export { ParsingErrorCallout } from './parsing_error_callout'; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx deleted file mode 100644 index 7db3659564ce2..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ /dev/null @@ -1,185 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useEffect } from 'react'; -import { EuiCallOut, htmlIdGenerator } from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Typeahead } from './typeahead'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { useSearchText, useUrlParams } from '../../../hooks'; -import { - esKuery, - IIndexPattern, - QuerySuggestion, - DataPublicPluginStart, -} from '../../../../../../../src/plugins/data/public'; -import { useIndexPattern } from './use_index_pattern'; - -const Container = styled.div` - margin-bottom: 4px; - position: relative; -`; - -interface State { - suggestions: QuerySuggestion[]; - isLoadingIndexPattern: boolean; -} - -function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { - const ast = esKuery.fromKueryExpression(kuery); - return esKuery.toElasticsearchQuery(ast, indexPattern); -} - -interface Props { - 'aria-label': string; - defaultKuery?: string; - 'data-test-subj': string; - shouldUpdateUrl?: boolean; - updateDefaultKuery?: (value: string) => void; -} - -export function KueryBar({ - 'aria-label': ariaLabel, - defaultKuery, - 'data-test-subj': dataTestSubj, - shouldUpdateUrl, - updateDefaultKuery, -}: Props) { - const { loading, index_pattern: indexPattern } = useIndexPattern(); - const { updateSearchText } = useSearchText(); - - const { - services: { - data: { autocomplete }, - }, - } = useKibana<{ data: DataPublicPluginStart }>(); - - const [state, setState] = useState({ - suggestions: [], - isLoadingIndexPattern: true, - }); - const [suggestionLimit, setSuggestionLimit] = useState(15); - const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); - let currentRequestCheck: string; - - const [getUrlParams, updateUrlParams] = useUrlParams(); - const { search: kuery, query } = getUrlParams(); - - useEffect(() => { - updateSearchText(kuery); - }, [kuery, updateSearchText]); - - useEffect(() => { - if (updateDefaultKuery && kuery) { - updateDefaultKuery(kuery); - } else if (defaultKuery && updateDefaultKuery) { - updateDefaultKuery(defaultKuery); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const indexPatternMissing = loading && !indexPattern; - - async function onChange(inputValue: string, selectionStart: number | null) { - if (!indexPattern) { - return; - } - - setIsLoadingSuggestions(true); - setState({ ...state, suggestions: [] }); - setSuggestionLimit(15); - - const currentRequest = htmlIdGenerator()(); - currentRequestCheck = currentRequest; - - try { - const suggestions = ( - (await autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - query: inputValue, - selectionStart: selectionStart || 0, - selectionEnd: selectionStart || 0, - useTimeRange: true, - })) || [] - ).filter((suggestion: QuerySuggestion) => !suggestion.text.startsWith('span.')); - if (currentRequest !== currentRequestCheck) { - return; - } - - setIsLoadingSuggestions(false); - setState({ ...state, suggestions }); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error while fetching suggestions', e); - } - } - - function onSubmit(inputValue: string) { - if (indexPattern === null) { - return; - } - - try { - const res = convertKueryToEsQuery(inputValue, indexPattern); - if (!res) { - return; - } - - if (shouldUpdateUrl !== false) { - updateUrlParams({ search: inputValue.trim() }); - } - updateSearchText(inputValue); - if (updateDefaultKuery) { - updateDefaultKuery(inputValue); - } - } catch (e) { - console.log('Invalid kuery syntax'); // eslint-disable-line no-console - } - } - - const increaseLimit = () => { - setSuggestionLimit(suggestionLimit + 15); - }; - - return ( - - - - {indexPatternMissing && !loading && ( - - -
    - } - color="warning" - iconType="alert" - size="s" - /> - )} - - ); -} diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx deleted file mode 100644 index 2e7dfe990e9c1..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx +++ /dev/null @@ -1,71 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { render } from '../../../../../lib/helper/rtl_helpers'; -import { SearchType } from './search_type'; - -describe('Kuery bar search type', () => { - it('can change from simple to kq;', () => { - let kqlSyntax = false; - const setKqlSyntax = jest.fn((val: boolean) => { - kqlSyntax = val; - }); - - const { getByTestId } = render( - - ); - - // open popover to change - fireEvent.click(getByTestId('syntaxChangeToKql')); - - // change syntax - fireEvent.click(getByTestId('toggleKqlSyntax')); - - expect(setKqlSyntax).toHaveBeenCalledWith(true); - expect(setKqlSyntax).toHaveBeenCalledTimes(1); - }); - - it('can change from kql to simple;', () => { - let kqlSyntax = false; - const setKqlSyntax = jest.fn((val: boolean) => { - kqlSyntax = val; - }); - - const { getByTestId } = render( - - ); - - fireEvent.click(getByTestId('syntaxChangeToKql')); - - fireEvent.click(getByTestId('toggleKqlSyntax')); - - expect(setKqlSyntax).toHaveBeenCalledWith(true); - expect(setKqlSyntax).toHaveBeenCalledTimes(1); - }); - - it('clears the query on change to kql', () => { - const setKqlSyntax = jest.fn(); - - const { history } = render(, { - url: '/app/uptime?query=test', - }); - - expect(history?.location.search).toBe(''); - }); - - it('clears the search param on change to simple syntax', () => { - const setKqlSyntax = jest.fn(); - - const { history } = render(, { - url: '/app/uptime?search=test', - }); - - expect(history?.location.search).toBe(''); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx deleted file mode 100644 index af539e1c361a1..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx +++ /dev/null @@ -1,144 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { - EuiPopover, - EuiFormRow, - EuiSwitch, - EuiButtonEmpty, - EuiPopoverTitle, - EuiText, - EuiSpacer, - EuiLink, - EuiButtonIcon, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { useUrlParams } from '../../../../../hooks'; -import { - CHANGE_SEARCH_BAR_SYNTAX, - CHANGE_SEARCH_BAR_SYNTAX_SIMPLE, - SYNTAX_OPTIONS_LABEL, -} from '../translations'; - -const BoxesVerticalIcon = euiStyled(EuiButtonIcon)` - padding: 10px 8px 0 8px; - border-radius: 0; - height: 38px; - width: 32px; - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - padding-top: 8px; - padding-bottom: 8px; - cursor: pointer; -`; - -interface Props { - kqlSyntax: boolean; - setKqlSyntax: (val: boolean) => void; -} - -export const SearchType = ({ kqlSyntax, setKqlSyntax }: Props) => { - const { - services: { docLinks }, - } = useKibana(); - - const [getUrlParams, updateUrlParams] = useUrlParams(); - - const { query, search } = getUrlParams(); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const onButtonClick = () => setIsPopoverOpen((prevState) => !prevState); - - const closePopover = () => setIsPopoverOpen(false); - - useEffect(() => { - if (kqlSyntax && query) { - updateUrlParams({ query: '' }); - } - - if (!kqlSyntax && search) { - updateUrlParams({ search: '' }); - } - }, [kqlSyntax, query, search, updateUrlParams]); - - const button = kqlSyntax ? ( - - KQL - - ) : ( - - ); - - return ( - -
    - {SYNTAX_OPTIONS_LABEL} - -

    - -

    -
    - - - setKqlSyntax(!kqlSyntax)} - data-test-subj="toggleKqlSyntax" - /> - -
    -
    - ); -}; - -const KqlDescription = ({ href }: { href: string }) => { - return ( - - {KIBANA_QUERY_LANGUAGE} - - ), - searchField: Monitor Name, ID, Url, - }} - /> - ); -}; - -const KIBANA_QUERY_LANGUAGE = i18n.translate('xpack.uptime.query.queryBar.kqlFullLanguageName', { - defaultMessage: 'Kibana Query Language', -}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx deleted file mode 100644 index a41fa656ec63d..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx +++ /dev/null @@ -1,89 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useRef, useEffect, RefObject } from 'react'; -import { EuiSuggestItem } from '@elastic/eui'; - -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; - -const SuggestionItem = euiStyled.div<{ selected: boolean }>` - background: ${(props) => (props.selected ? props.theme.eui.euiColorLightestShade : 'initial')}; -`; - -function getIconColor(type: string) { - switch (type) { - case 'field': - return 'tint5'; - case 'value': - return 'tint0'; - case 'operator': - return 'tint1'; - case 'conjunction': - return 'tint3'; - case 'recentSearch': - return 'tint10'; - default: - return 'tint5'; - } -} - -function getEuiIconType(type: string) { - switch (type) { - case 'field': - return 'kqlField'; - case 'value': - return 'kqlValue'; - case 'recentSearch': - return 'search'; - case 'conjunction': - return 'kqlSelector'; - case 'operator': - return 'kqlOperand'; - default: - throw new Error(`Unknown type ${type}`); - } -} - -interface SuggestionProps { - onClick: (sug: QuerySuggestion) => void; - onMouseEnter: () => void; - selected: boolean; - suggestion: QuerySuggestion; - innerRef: (node: any) => void; -} - -export const Suggestion: React.FC = ({ - innerRef, - selected, - suggestion, - onClick, - onMouseEnter, -}) => { - const childNode: RefObject = useRef(null); - - useEffect(() => { - if (childNode.current) { - innerRef(childNode.current); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [childNode]); - - return ( - - onClick(suggestion)} - onMouseEnter={onMouseEnter} - // @ts-ignore - description={suggestion.description} - /> - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx deleted file mode 100644 index 9b382772346d1..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx +++ /dev/null @@ -1,146 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useRef, useState, useEffect } from 'react'; -import { isEmpty } from 'lodash'; -import { rgba } from 'polished'; -import { Suggestion } from './suggestion'; -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; - -export const unit = 16; - -export const units = { - unit, - eighth: unit / 8, - quarter: unit / 4, - half: unit / 2, - minus: unit * 0.75, - plus: unit * 1.5, - double: unit * 2, - triple: unit * 3, - quadruple: unit * 4, -}; - -export function px(value: number): string { - return `${value}px`; -} - -const List = euiStyled.ul` - width: 100%; - border: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; - border-radius: ${px(units.quarter)}; - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - z-index: 10; - max-height: ${px(unit * 20)}; - overflow: auto; - position: absolute; - - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } -`; - -interface SuggestionsProps { - index: number; - onClick: (sug: QuerySuggestion) => void; - onMouseEnter: (index: number) => void; - show?: boolean; - suggestions: QuerySuggestion[]; - loadMore: () => void; -} - -export const Suggestions: React.FC = ({ - show, - index, - onClick, - suggestions, - onMouseEnter, - loadMore, -}) => { - const [childNodes, setChildNodes] = useState([]); - - const parentNode = useRef(null); - - useEffect(() => { - const scrollIntoView = () => { - const parent = parentNode.current; - const child = childNodes[index]; - - if (index == null || !parent || !child) { - return; - } - - const scrollTop = Math.max( - Math.min(parent.scrollTop, child.offsetTop), - child.offsetTop + child.offsetHeight - parent.offsetHeight - ); - - parent.scrollTop = scrollTop; - }; - scrollIntoView(); - }, [index, childNodes]); - - if (!show || isEmpty(suggestions)) { - return null; - } - - const handleScroll = () => { - const parent = parentNode.current; - - if (!loadMore || !parent) { - return; - } - - const position = parent.scrollTop + parent.offsetHeight; - const height = parent.scrollHeight; - const remaining = height - position; - const margin = 50; - - if (!height || !position) { - return; - } - if (remaining <= margin) { - loadMore(); - } - }; - - const suggestionsNodes = suggestions.map((suggestion, currIndex) => { - const key = suggestion + '_' + currIndex; - return ( - { - const nodes = childNodes; - nodes[currIndex] = node; - setChildNodes([...nodes]); - }} - selected={currIndex === index} - suggestion={suggestion} - onClick={onClick} - onMouseEnter={() => onMouseEnter(currIndex)} - key={key} - /> - ); - }); - - return ( - - {suggestionsNodes} - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx deleted file mode 100644 index ed75747aa3416..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { Typeahead } from './typehead'; -import { render } from '../../../../lib/helper/rtl_helpers'; - -describe('Type head', () => { - jest.useFakeTimers(); - - it('it sets initial value', () => { - const { getByTestId, getByDisplayValue, history } = render( - {}} - suggestions={[]} - loadMore={() => {}} - queryExample="" - /> - ); - - const input = getByTestId('uptimeKueryBarInput'); - - expect(input).toBeInTheDocument(); - expect(getByDisplayValue('elastic')).toBeInTheDocument(); - - fireEvent.change(input, { target: { value: 'kibana' } }); - - // to check if it updateds the query params, needed for debounce wait - jest.advanceTimersByTime(250); - - expect(history.location.search).toBe('?query=kibana'); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx deleted file mode 100644 index e4dd175b2fe1b..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx +++ /dev/null @@ -1,210 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ChangeEvent, MouseEvent, useState, useRef, useEffect } from 'react'; -import { EuiFieldSearch, EuiProgress, EuiOutsideClickDetector } from '@elastic/eui'; -import { Suggestions } from './suggestions'; -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; -import { SearchType } from './search_type/search_type'; -import { useKqlSyntax } from './use_kql_syntax'; -import { useKeyEvents } from './use_key_events'; -import { KQL_PLACE_HOLDER, SIMPLE_SEARCH_PLACEHOLDER } from './translations'; -import { useSimpleQuery } from './use_simple_kuery'; - -interface TypeaheadProps { - onChange: (inputValue: string, selectionStart: number | null) => void; - onSubmit: (inputValue: string) => void; - suggestions: QuerySuggestion[]; - queryExample: string; - initialValue?: string; - isLoading?: boolean; - disabled?: boolean; - dataTestSubj: string; - ariaLabel: string; - loadMore: () => void; -} - -export const Typeahead: React.FC = ({ - initialValue, - suggestions, - onChange, - onSubmit, - dataTestSubj, - ariaLabel, - disabled, - isLoading, - loadMore, -}) => { - const [value, setValue] = useState(''); - const [index, setIndex] = useState(null); - const [isSuggestionsVisible, setIsSuggestionsVisible] = useState(false); - - const [selected, setSelected] = useState(null); - const [inputIsPristine, setInputIsPristine] = useState(true); - const [lastSubmitted, setLastSubmitted] = useState(''); - - const { kqlSyntax, setKqlSyntax } = useKqlSyntax({ setValue }); - - const inputRef = useRef(); - - const { setQuery } = useSimpleQuery(); - - useEffect(() => { - if (inputIsPristine && initialValue) { - setValue(initialValue); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialValue]); - - const selectSuggestion = (suggestion: QuerySuggestion) => { - const nextInputValue = - value.substr(0, suggestion.start) + suggestion.text + value.substr(suggestion.end); - - setValue(nextInputValue); - setSelected(suggestion); - setIndex(null); - - onChange(nextInputValue, nextInputValue.length); - }; - - const { onKeyDown, onKeyUp } = useKeyEvents({ - index, - value, - isSuggestionsVisible, - setIndex, - setIsSuggestionsVisible, - suggestions, - selectSuggestion, - onChange, - onSubmit, - }); - - const onClickOutside = () => { - if (isSuggestionsVisible) { - setIsSuggestionsVisible(false); - onSuggestionSubmit(); - } - }; - - const onChangeInputValue = (event: ChangeEvent) => { - const { value: valueN, selectionStart } = event.target; - const hasValue = Boolean(valueN.trim()); - - setValue(valueN); - - setInputIsPristine(false); - setIndex(null); - - if (!kqlSyntax) { - setQuery(valueN); - return; - } - - setIsSuggestionsVisible(hasValue); - - if (!hasValue) { - onSubmit(valueN); - } - onChange(valueN, selectionStart!); - }; - - const onClickInput = (event: MouseEvent & ChangeEvent) => { - if (kqlSyntax) { - event.stopPropagation(); - const { selectionStart } = event.target; - onChange(value, selectionStart!); - } - }; - - const onFocus = () => { - if (kqlSyntax) { - setIsSuggestionsVisible(true); - } - }; - - const onClickSuggestion = (suggestion: QuerySuggestion) => { - selectSuggestion(suggestion); - if (inputRef.current) inputRef.current.focus(); - }; - - const onMouseEnterSuggestion = (indexN: number) => { - setIndex(indexN); - }; - - const onSuggestionSubmit = () => { - if ( - lastSubmitted !== value && - selected && - (selected.type === 'value' || selected.text.trim() === ': *') - ) { - onSubmit(value); - - setLastSubmitted(value); - setSelected(null); - } - }; - - return ( - - -
    - { - if (node) { - inputRef.current = node; - } - }} - disabled={disabled} - value={value} - onKeyDown={kqlSyntax ? onKeyDown : undefined} - onKeyUp={kqlSyntax ? onKeyUp : undefined} - onFocus={onFocus} - onChange={onChangeInputValue} - onClick={onClickInput} - autoComplete="off" - spellCheck={false} - data-test-subj={'uptimeKueryBarInput'} - append={} - /> - - {isLoading && ( - - )} -
    - {kqlSyntax && ( - - )} -
    -
    - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts deleted file mode 100644 index ac702cc95dd64..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ChangeEvent, KeyboardEvent } from 'react'; -import * as React from 'react'; -import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; - -const KEY_CODES = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ENTER: 13, - ESC: 27, - TAB: 9, -}; - -interface Props { - value: string; - index: number | null; - isSuggestionsVisible: boolean; - setIndex: React.Dispatch>; - setIsSuggestionsVisible: React.Dispatch>; - suggestions: QuerySuggestion[]; - selectSuggestion: (suggestion: QuerySuggestion) => void; - onChange: (inputValue: string, selectionStart: number | null) => void; - onSubmit: (inputValue: string) => void; -} - -export const useKeyEvents = ({ - value, - index, - isSuggestionsVisible, - setIndex, - setIsSuggestionsVisible, - suggestions, - selectSuggestion, - onChange, - onSubmit, -}: Props) => { - const incrementIndex = (currentIndex: number) => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= suggestions.length) { - nextIndex = 0; - } - - setIndex(nextIndex); - }; - - const decrementIndex = (currentIndex: number) => { - let previousIndex: number | null = currentIndex - 1; - if (previousIndex < 0) { - previousIndex = null; - } - setIndex(previousIndex); - }; - - const onKeyUp = (event: KeyboardEvent & ChangeEvent) => { - const { selectionStart } = event.target; - switch (event.keyCode) { - case KEY_CODES.LEFT: - setIsSuggestionsVisible(true); - onChange(value, selectionStart); - break; - case KEY_CODES.RIGHT: - setIsSuggestionsVisible(true); - onChange(value, selectionStart); - break; - } - }; - - const onKeyDown = (event: KeyboardEvent) => { - switch (event.keyCode) { - case KEY_CODES.DOWN: - event.preventDefault(); - if (isSuggestionsVisible) { - incrementIndex(index!); - } else { - setIndex(0); - setIsSuggestionsVisible(true); - } - break; - case KEY_CODES.UP: - event.preventDefault(); - if (isSuggestionsVisible) { - decrementIndex(index!); - } - break; - case KEY_CODES.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && suggestions[index!]) { - selectSuggestion(suggestions[index!]); - } else { - setIsSuggestionsVisible(false); - onSubmit(value); - } - break; - case KEY_CODES.ESC: - event.preventDefault(); - setIsSuggestionsVisible(false); - break; - case KEY_CODES.TAB: - setIsSuggestionsVisible(false); - break; - } - }; - - return { onKeyUp, onKeyDown }; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts deleted file mode 100644 index 2c945c33b9dc7..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts +++ /dev/null @@ -1,56 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { KQL_SYNTAX_LOCAL_STORAGE } from '../../../../../common/constants'; -import { useUrlParams } from '../../../../hooks'; - -interface Props { - setValue: React.Dispatch>; -} - -export const useKqlSyntax = ({ setValue }: Props) => { - const [kqlSyntax, setKqlSyntax] = useState( - localStorage.getItem(KQL_SYNTAX_LOCAL_STORAGE) === 'true' - ); - - const [getUrlParams] = useUrlParams(); - - const { query, search } = getUrlParams(); - - useEffect(() => { - setValue(query || ''); - }, [query, setValue]); - - useEffect(() => { - setValue(search || ''); - }, [search, setValue]); - - useEffect(() => { - if (query || search) { - // if url has query or params we will give them preference on load - // for selecting syntax type - if (query) { - setKqlSyntax(false); - } - if (search) { - setKqlSyntax(true); - } - } else { - setKqlSyntax(localStorage.getItem(KQL_SYNTAX_LOCAL_STORAGE) === 'true'); - } - // This part is meant to run only when component loads - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - localStorage.setItem(KQL_SYNTAX_LOCAL_STORAGE, String(kqlSyntax)); - setValue(''); - }, [kqlSyntax, setValue]); - - return { kqlSyntax, setKqlSyntax }; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts deleted file mode 100644 index 55df62a7e14d6..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts +++ /dev/null @@ -1,32 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; -import { useUrlParams } from '../../../../hooks'; - -export const useSimpleQuery = () => { - const [getUrlParams, updateUrlParams] = useUrlParams(); - - const { query } = getUrlParams(); - - const [debouncedValue, setDebouncedValue] = useState(query ?? ''); - - useEffect(() => { - setDebouncedValue(query ?? ''); - }, [query]); - - useDebounce( - () => { - updateUrlParams({ query: debouncedValue }); - }, - 250, - [debouncedValue] - ); - - return { query, setQuery: setDebouncedValue }; -}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap index 779b513915ea8..dc125ec4b8466 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap @@ -831,7 +831,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` } .c3 { - padding-top: 12px; + margin-top: 12px; } .c0 { @@ -1652,7 +1652,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` >