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 b9afc197bac9c..a8dcafeb7753c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -107,7 +107,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 @@ -164,7 +163,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 @@ -201,9 +199,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 @@ -213,9 +208,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/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.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/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/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/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 73b268e1e48b3..643718b961650 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:` 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/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..a1acf73ea26f0 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", @@ -205,7 +206,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", @@ -240,7 +240,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 +443,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 +534,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 +681,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-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-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 a027768ad66a0..249183d4b1e31 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 202261 + lists: 228500 logstash: 53548 management: 46112 maps: 80000 @@ -68,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-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.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index df46753747f5b..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'; 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 67b5393f0b838..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,71 +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: [], - version: '8.0.0', - 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( @@ -117,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, @@ -216,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/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 fb5fe3efd3e06..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 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/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/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/jest.config.js b/src/legacy/server/jest.config.js deleted file mode 100644 index 0a7322d2985fa..0000000000000 --- a/src/legacy/server/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/server'], -}; 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/jest.config.js b/src/legacy/utils/jest.config.js deleted file mode 100644 index 593c3aec9d0b0..0000000000000 --- a/src/legacy/utils/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/utils'], -}; 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/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/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/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/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/public/public.api.md b/src/plugins/data/public/public.api.md index 7f243cefd08b6..ec24a9296674d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2022,6 +2022,8 @@ export const QueryStringInput: (props: QueryStringInputProps) => JSX.Element; // // @public (undocumented) export interface QueryStringInputProps { + // (undocumented) + autoSubmit?: boolean; // (undocumented) bubbleSubmitEvent?: boolean; // (undocumented) @@ -2071,6 +2073,8 @@ export interface QueryStringInputProps { // (undocumented) size?: SuggestionsListSize; // (undocumented) + storageKey?: string; + // (undocumented) submitOnBlur?: boolean; } 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/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 16e1325b2b56b..900a4ab7d7eb7 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 = () => { @@ -756,6 +778,9 @@ 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..9fff4ac95c87e 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; }; } @@ -1232,7 +1232,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/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/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..e0ca654c956c6 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, + }, } ), }; @@ -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/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/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 37b9380f6f2b9..63d136cf9445a 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -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/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/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/core/server/utils/index.ts b/src/plugins/vis_type_timeseries/public/application/contexts/default_index_context.ts similarity index 66% rename from src/core/server/utils/index.ts rename to src/plugins/vis_type_timeseries/public/application/contexts/default_index_context.ts index b0776c48f3bed..a8770d86fba9b 100644 --- a/src/core/server/utils/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/contexts/default_index_context.ts @@ -6,5 +6,7 @@ * Side Public License, v 1. */ -export * from './from_root'; -export * from './package_json'; +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_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 ( = ({ { 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/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/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/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/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({ ({ 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/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index fed938119c4a6..58fb096ca3a51 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -18,7 +18,12 @@ 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() { @@ -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 dbc207c9e6d26..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.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/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 8b81101fd2f39..5d5e6eebb4c9f 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,7 +5,6 @@ * 2.0. */ -import LRU from 'lru-cache'; import { IndexPatternsFetcher, FieldDescriptor, @@ -19,11 +18,6 @@ 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 = ({ context, @@ -33,11 +27,6 @@ export const getDynamicIndexPattern = ({ 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 indexPatternsFetcher = new IndexPatternsFetcher( context.core.elasticsearch.client.asCurrentUser ); @@ -57,11 +46,8 @@ 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( diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 13e70a2043cf0..87bc97d346984 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -120,6 +120,7 @@ export function createApi() { return response.ok({ body }); } catch (error) { + logger.error(error); const opts = { statusCode: 500, body: { 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/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/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/overview/kuery_bar/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts similarity index 81% rename from x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts index 22d195c6e4253..0bd18ea640850 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KueryBar } from './kuery_bar'; +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 7f4373835f8d5..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,13 +7,15 @@ 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'; @@ -26,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 ( <> 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/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx index ae91b9648356c..2d0cced3730ba 100644 --- 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 @@ -20,6 +20,7 @@ import { } 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'; @@ -48,7 +49,7 @@ export const SampleResponse: React.FC = () => { - {/* TODO */} + 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/typeahead/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/typeahead/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts index c41b3a2bf5eb5..e6cd2516fc03a 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/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 { Typeahead } from './typehead'; +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.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 7a76de43be41b..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 @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - useEffect(() => { - 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`. - setContext(isOrganization); + 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`. + + setContext(isOrganization); + + useEffect(() => { setChromeIsVisible(isOrganization); }, [pathname]); 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/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 9e3b50ea083eb..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,6 +15,7 @@ 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'; @@ -43,6 +44,7 @@ describe('PrivateSourcesLayout', () => { 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 2a6281075dc40..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,6 +12,7 @@ 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'; @@ -48,22 +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/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index abab139e32369..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,14 +20,18 @@ .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; } } 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/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 > ; interface Props { actions: CreateAnalyticsFormProps['actions']; 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_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 && ( = ({ anomalyData }) => { = ({ overlayKey, start, end, color, showMar /> 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/observability/public/components/app/section/metrics/host_link.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/host_link.tsx new file mode 100644 index 0000000000000..921cec4222ea0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/host_link.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { StringOrNull } from '../../../..'; + +import aixLogo from './logos/aix.svg'; +import androidLogo from './logos/android.svg'; +import darwinLogo from './logos/darwin.svg'; +import dragonflyLogo from './logos/dragonfly.svg'; +import freebsdLogo from './logos/freebsd.svg'; +import illumosLogo from './logos/illumos.svg'; +import linuxLogo from './logos/linux.svg'; +import solarisLogo from './logos/solaris.svg'; +import netbsdLogo from './logos/netbsd.svg'; + +interface Props { + name: StringOrNull; + id: StringOrNull; + provider: StringOrNull; + platform: StringOrNull; + timerange: { from: number; to: number }; +} + +export function HostLink({ name, id, provider, platform, timerange }: Props) { + const providerLogo = + provider === 'aws' + ? 'logoAWS' + : provider === 'gcp' + ? 'logoGCP' + : provider === 'azure' + ? 'logoAzure' + : 'compute'; + + const platformLogo = + platform === 'darwin' + ? darwinLogo + : platform === 'windows' + ? 'logoWindows' + : platform === 'linux' + ? linuxLogo + : platform === 'aix' + ? aixLogo + : platform === 'andriod' + ? androidLogo + : platform === 'dragonfly' + ? dragonflyLogo + : platform === 'illumos' + ? illumosLogo + : platform === 'freebsd' + ? freebsdLogo + : platform === 'solaris' + ? solarisLogo + : platform === 'netbsd' + ? netbsdLogo + : 'empty'; + const link = `../../app/metrics/link-to/host-detail/${id}?from=${timerange.from}&to=${timerange.to}`; + return ( + <> + {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/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 7e99874f557b3..b90d5115bc41e 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,7 +6,6 @@ */ 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'; @@ -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/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index 868e5be2b6317..bba2083aceb80 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -321,56 +321,18 @@ describe('registerDataHandler', () => { }); 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/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 e9960833a1c4f..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; @@ -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/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/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 = ({ > = ({ > = ({ > = ({ > = ({ > { + 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..277226c81c925 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 @@ -124,7 +124,10 @@ export const AdvancedRuntimeMappingsSettings: FC = (props) = - + {(copy: () => void) => ( ', () => { }, }; - 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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6dc490b4ffc53..0c16860acf56c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8169,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": "選択した項目をクリア", @@ -8730,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}", @@ -16660,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": "ダウン", @@ -19727,16 +19716,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": "コメントを追加しました", @@ -22954,7 +22933,6 @@ "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": "詳細については、ドキュメンテーションを参照してください。", @@ -23094,9 +23072,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}秒", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 32574690b13f2..5e5f53356a2e8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8242,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 {# 个代理}}", @@ -8818,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}", @@ -16886,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": "关闭", @@ -20022,16 +20011,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": "已添加注释", @@ -23313,7 +23292,6 @@ "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": "在我们的文档中获取更多的信息。", @@ -23453,9 +23431,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} 秒", 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/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/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index aea4ce947511e..426d3f1f10db8 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -14,12 +14,5 @@ "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": [ - "observability", - "kibanaReact", - "home", - "data", - "ml", - "maps" - ] + "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "maps"] } 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/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/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`] = ` >