diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index e4b5e35e1e4a9..5317b2c500b49 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -2,5 +2,8 @@ set -e +# cache image used by kibana-load-testing project +docker pull "maven:3.6.3-openjdk-8-slim" + ./.ci/packer_cache_for_branch.sh master ./.ci/packer_cache_for_branch.sh 7.x diff --git a/.eslintrc.js b/.eslintrc.js index b70090a50e64d..a7b45534391c0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -89,6 +89,73 @@ const SAFER_LODASH_SET_DEFINITELYTYPED_HEADER = ` */ `; +/** Packages which should not be included within production code. */ +const DEV_PACKAGES = [ + 'kbn-babel-code-parser', + 'kbn-dev-utils', + 'kbn-cli-dev-mode', + 'kbn-docs-utils', + 'kbn-es*', + 'kbn-eslint*', + 'kbn-optimizer', + 'kbn-plugin-generator', + 'kbn-plugin-helpers', + 'kbn-pm', + 'kbn-storybook', + 'kbn-telemetry-tools', + 'kbn-test', +]; + +/** Directories (at any depth) which include dev-only code. */ +const DEV_DIRECTORIES = [ + '.storybook', + '__tests__', + '__test__', + '__jest__', + '__fixtures__', + '__mocks__', + '__stories__', + 'e2e', + 'fixtures', + 'ftr_e2e', + 'integration_tests', + 'manual_tests', + 'mock', + 'storybook', + 'scripts', + 'test', + 'test-d', + 'test_utils', + 'test_utilities', + 'test_helpers', + 'tests_client_integration', +]; + +/** File patterns for dev-only code. */ +const DEV_FILE_PATTERNS = [ + '*.mock.{js,ts,tsx}', + '*.test.{js,ts,tsx}', + '*.test.helpers.{js,ts,tsx}', + '*.stories.{js,ts,tsx}', + '*.story.{js,ts,tsx}', + '*.stub.{js,ts,tsx}', + 'mock.{js,ts,tsx}', + '_stubs.{js,ts,tsx}', + '{testHelpers,test_helper,test_utils}.{js,ts,tsx}', + '{postcss,webpack}.config.js', +]; + +/** Glob patterns which describe dev-only code. */ +const DEV_PATTERNS = [ + ...DEV_PACKAGES.map((pkg) => `packages/${pkg}/**/*`), + ...DEV_DIRECTORIES.map((dir) => `{packages,src,x-pack}/**/${dir}/**/*`), + ...DEV_FILE_PATTERNS.map((file) => `{packages,src,x-pack}/**/${file}`), + 'packages/kbn-interpreter/tasks/**/*', + 'src/dev/**/*', + 'x-pack/{dev-tools,tasks,scripts,test,build_chromium}/**/*', + 'x-pack/plugins/*/server/scripts/**/*', +]; + module.exports = { root: true, @@ -491,43 +558,17 @@ module.exports = { }, /** - * Files that ARE NOT allowed to use devDependencies - */ - { - files: ['x-pack/**/*.js', 'packages/kbn-interpreter/**/*.js'], - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: false, - peerDependencies: true, - packageDir: '.', - }, - ], - }, - }, - - /** - * Files that ARE allowed to use devDependencies + * Single package.json rules, it tells eslint to ignore the child package.json files + * and look for dependencies declarations in the single and root level package.json */ { - files: [ - 'packages/kbn-es/src/**/*.js', - 'packages/kbn-interpreter/tasks/**/*.js', - 'packages/kbn-interpreter/src/plugin/**/*.js', - 'x-pack/{dev-tools,tasks,scripts,test,build_chromium}/**/*.js', - 'x-pack/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__,public}/**/*.js', - 'x-pack/**/*.test.js', - 'x-pack/test_utils/**/*', - 'x-pack/gulpfile.js', - 'x-pack/plugins/apm/public/utils/testHelpers.js', - 'x-pack/plugins/canvas/shareable_runtime/postcss.config.js', - ], + files: ['{src,x-pack,packages}/**/*.{js,mjs,ts,tsx}'], rules: { 'import/no-extraneous-dependencies': [ 'error', { - devDependencies: true, + /* Files that ARE allowed to use devDependencies */ + devDependencies: [...DEV_PATTERNS], peerDependencies: true, packageDir: '.', }, @@ -1420,21 +1461,5 @@ module.exports = { ], }, }, - - /** - * Single package.json rules, it tells eslint to ignore the child package.json files - * and look for dependencies declarations in the single and root level package.json - */ - { - files: ['**/*.{js,mjs,ts,tsx}'], - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - packageDir: '.', - }, - ], - }, - }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f2f260addb35..f27885c1e32c3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,6 +59,7 @@ /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-services /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services /x-pack/plugins/runtime_fields @elastic/kibana-app-services +/x-pack/test/search_sessions_integration/ @elastic/kibana-app-services #CC# /src/plugins/bfetch/ @elastic/kibana-app-services #CC# /src/plugins/index_pattern_management/ @elastic/kibana-app-services #CC# /src/plugins/inspector/ @elastic/kibana-app-services @@ -118,18 +119,23 @@ # Machine Learning /x-pack/plugins/ml/ @elastic/ml-ui +/x-pack/test/accessibility/apps/ml.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @elastic/ml-ui +/x-pack/test/api_integration/apis/ml/ @elastic/ml-ui +/x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui /x-pack/test/functional/apps/ml/ @elastic/ml-ui +/x-pack/test/functional/es_archives/ml/ @elastic/ml-ui /x-pack/test/functional/services/ml/ @elastic/ml-ui -/x-pack/test/accessibility/apps/ml.ts @elastic/ml-ui +/x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui +/x-pack/test/functional_with_es_ssl/apps/ml/ @elastic/ml-ui + # ML team owns and maintains the transform plugin despite it living in the Elasticsearch management section. /x-pack/plugins/transform/ @elastic/ml-ui -/x-pack/test/functional/apps/transform/ @elastic/ml-ui -/x-pack/test/functional/services/transform/ @elastic/ml-ui /x-pack/test/accessibility/apps/transform.ts @elastic/ml-ui -/x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui -/x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui - +/x-pack/test/api_integration/apis/transform/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui +/x-pack/test/functional/apps/transform/ @elastic/ml-ui +/x-pack/test/functional/services/transform/ @elastic/ml-ui /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui # Maps @@ -157,6 +163,7 @@ /packages/kbn-ui-shared-deps/ @elastic/kibana-operations /packages/kbn-es-archiver/ @elastic/kibana-operations /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 @@ -193,6 +200,8 @@ /packages/kbn-config/ @elastic/kibana-core /packages/kbn-logging/ @elastic/kibana-core /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 diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md deleted file mode 100644 index 38fcb7af30b47..0000000000000 --- a/.github/ISSUE_TEMPLATE/Question.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Question -about: Who, what, when, where, and how? - ---- - -Hey, stop right there! - -We use GitHub to track feature requests and bug reports. Please do not submit issues for questions about how to use features of Kibana, how to set Kibana up, best practices, or development related help. - -However, we do want to help! Head on over to our official Kibana forums and ask your questions there. In additional to awesome, knowledgeable community contributors, core Kibana developers are on the forums every single day to help you out. - -The forums are here: https://discuss.elastic.co/c/kibana - -We can't stop you from opening an issue here, but it will likely linger without a response for days or weeks before it is closed and we ask you to join us on the forums instead. Save yourself the time, and ask on the forums today. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..348d756c141b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Question + url: https://discuss.elastic.co/c/kibana + about: Please ask and answer questions here. diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 9f0e6e0231feb..4639414b4564e 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "55a25a762fcf9c9b88ab54436581e671bc9f4f523cb5a1bd32459ebec7be68a8", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.2/rules_nodejs-3.2.2.tar.gz"], + sha256 = "dd7ea7efda7655c218ca707f55c3e1b9c68055a70c31a98f264b3445bc8f4cb1", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.3/rules_nodejs-3.2.3.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.2.2") +check_rules_nodejs_version(minimum_version_string = "3.2.3") # Setup the Node.js toolchain for the architectures we want to support # diff --git a/api_docs/alerting.json b/api_docs/alerting.json index 5550798c4316f..f6e37bafdedb0 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -810,7 +810,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", - "lineNumber": 132 + "lineNumber": 133 } }, { @@ -821,7 +821,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", - "lineNumber": 133 + "lineNumber": 134 } }, { @@ -832,7 +832,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", - "lineNumber": 134 + "lineNumber": 135 } }, { @@ -843,7 +843,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", - "lineNumber": 135 + "lineNumber": 136 }, "signature": [ "Pick<", @@ -860,7 +860,7 @@ ], "source": { "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", - "lineNumber": 131 + "lineNumber": 132 }, "initialIsOpen": false }, @@ -905,7 +905,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 111 + "lineNumber": 93 } } ], @@ -913,13 +913,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 103 + "lineNumber": 85 } } ], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 102 + "lineNumber": 84 }, "initialIsOpen": false }, @@ -938,7 +938,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 123 + "lineNumber": 105 }, "signature": [ "() => Set<", @@ -994,7 +994,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 124 + "lineNumber": 106 } } ], @@ -1002,7 +1002,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 124 + "lineNumber": 106 } }, { @@ -1013,7 +1013,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 125 + "lineNumber": 107 }, "signature": [ "() => Promise<", @@ -1030,7 +1030,7 @@ ], "source": { "path": "x-pack/plugins/alerting/server/plugin.ts", - "lineNumber": 122 + "lineNumber": 104 }, "initialIsOpen": false } @@ -1118,7 +1118,7 @@ }, ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"muteAll\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"mutedInstanceIds\" | \"executionStatus\">>; delete: ({ id }: { id: string; }) => Promise<{}>; create: = never>({ data, options, }: ", "CreateOptions", - ") => Promise<", + ") => Promise>; find: ({ options: { fields, ...options }, }?: { options?: ", + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"muteAll\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"mutedInstanceIds\" | \"executionStatus\">>; find: ({ options: { fields, ...options }, }?: { options?: ", "FindOptions", " | undefined; }) => Promise<", { @@ -2922,7 +2922,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/common/index.ts", - "lineNumber": 28 + "lineNumber": 30 }, "signature": [ "\"alerts\"" @@ -3006,16 +3006,16 @@ }, { "tags": [], - "id": "def-common.BASE_ALERT_API_PATH", + "id": "def-common.BASE_ALERTING_API_PATH", "type": "string", - "label": "BASE_ALERT_API_PATH", + "label": "BASE_ALERTING_API_PATH", "description": [], "source": { "path": "x-pack/plugins/alerting/common/index.ts", - "lineNumber": 27 + "lineNumber": 28 }, "signature": [ - "\"/api/alerts\"" + "\"/api/alerting\"" ], "initialIsOpen": false }, @@ -3034,6 +3034,36 @@ ], "initialIsOpen": false }, + { + "tags": [], + "id": "def-common.INTERNAL_BASE_ALERTING_API_PATH", + "type": "string", + "label": "INTERNAL_BASE_ALERTING_API_PATH", + "description": [], + "source": { + "path": "x-pack/plugins/alerting/common/index.ts", + "lineNumber": 29 + }, + "signature": [ + "\"/internal/alerting\"" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.LEGACY_BASE_ALERT_API_PATH", + "type": "string", + "label": "LEGACY_BASE_ALERT_API_PATH", + "description": [], + "source": { + "path": "x-pack/plugins/alerting/common/index.ts", + "lineNumber": 27 + }, + "signature": [ + "\"/api/alerts\"" + ], + "initialIsOpen": false + }, { "id": "def-common.RawAlertInstance", "type": "Type", diff --git a/api_docs/apm.json b/api_docs/apm.json index ac26de577cf0f..7ddf4f2548d84 100644 --- a/api_docs/apm.json +++ b/api_docs/apm.json @@ -747,7 +747,7 @@ "text": "APMEventESSearchRequest" }, ">(params: TParams, { includeLegacyData }?: { includeLegacyData?: boolean | undefined; }): Promise<", - "ESSearchResponse", + "InferSearchResponseOf", " void; }" + "{ addProcessorDefinition: (processor: unknown) => void; }" ], "lifecycle": "start", "initialIsOpen": true diff --git a/api_docs/core.json b/api_docs/core.json index e02bd33d6671b..cb416cba80078 100644 --- a/api_docs/core.json +++ b/api_docs/core.json @@ -1431,7 +1431,7 @@ "description": [], "source": { "path": "src/core/public/doc_links/doc_links_service.ts", - "lineNumber": 300 + "lineNumber": 302 } }, { @@ -1442,7 +1442,7 @@ "description": [], "source": { "path": "src/core/public/doc_links/doc_links_service.ts", - "lineNumber": 301 + "lineNumber": 303 } }, { @@ -1453,7 +1453,7 @@ "description": [], "source": { "path": "src/core/public/doc_links/doc_links_service.ts", - "lineNumber": 302 + "lineNumber": 304 }, "signature": [ "{ readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; }; readonly auditbeat: { readonly base: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly enterpriseSearch: { readonly base: string; readonly appSearchBase: string; readonly workplaceSearchBase: string; }; readonly heartbeat: { readonly base: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; readonly histogram: string; readonly ip_range: string; readonly range: string; readonly significant_terms: string; readonly terms: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; readonly min_bucket: string; readonly sum_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative_sum: string; readonly derivative: string; readonly geo_bounds: string; readonly geo_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving_avg: string; readonly percentile_ranks: string; readonly serial_diff: string; readonly std_dev: string; readonly sum: string; readonly top_hits: string; }; readonly runtimeFields: string; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; }; readonly addData: string; readonly kibana: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; readonly gettingStarted: string; }; readonly query: { readonly eql: string; readonly luceneQuerySyntax: string; readonly queryDsl: string; readonly kueryQuerySyntax: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; readonly transforms: Record; readonly visualize: Record; readonly apis: Readonly<{ createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; readonly observability: Record; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly watcher: Record; readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; }" @@ -1462,7 +1462,7 @@ ], "source": { "path": "src/core/public/doc_links/doc_links_service.ts", - "lineNumber": 299 + "lineNumber": 301 }, "initialIsOpen": false }, @@ -3833,7 +3833,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 141 + "lineNumber": 142 }, "signature": [ "string | undefined" @@ -3842,7 +3842,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 139 + "lineNumber": 140 }, "initialIsOpen": false }, @@ -3865,7 +3865,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 78 + "lineNumber": 79 }, "signature": [ "string | string[]" @@ -3879,7 +3879,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 79 + "lineNumber": 80 }, "signature": [ "number | undefined" @@ -3893,7 +3893,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 80 + "lineNumber": 81 }, "signature": [ "number | undefined" @@ -3907,7 +3907,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 81 + "lineNumber": 82 }, "signature": [ "string | undefined" @@ -3916,15 +3916,15 @@ { "tags": [], "id": "def-public.SavedObjectsFindOptions.sortOrder", - "type": "string", + "type": "CompoundType", "label": "sortOrder", "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 82 + "lineNumber": 83 }, "signature": [ - "string | undefined" + "\"asc\" | \"desc\" | \"_doc\" | undefined" ] }, { @@ -3937,7 +3937,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 88 + "lineNumber": 89 }, "signature": [ "string[] | undefined" @@ -3953,7 +3953,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 90 + "lineNumber": 91 }, "signature": [ "string | undefined" @@ -3969,7 +3969,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 92 + "lineNumber": 93 }, "signature": [ "string[] | undefined" @@ -3985,10 +3985,10 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 96 + "lineNumber": 97 }, "signature": [ - "unknown[] | undefined" + "string[] | undefined" ] }, { @@ -4001,7 +4001,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 101 + "lineNumber": 102 }, "signature": [ "string[] | undefined" @@ -4017,7 +4017,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 107 + "lineNumber": 108 }, "signature": [ { @@ -4048,7 +4048,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 111 + "lineNumber": 112 }, "signature": [ "\"AND\" | \"OR\" | undefined" @@ -4064,7 +4064,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 116 + "lineNumber": 117 }, "signature": [ "\"AND\" | \"OR\" | undefined" @@ -4078,7 +4078,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 117 + "lineNumber": 118 }, "signature": [ "any" @@ -4092,7 +4092,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 118 + "lineNumber": 119 }, "signature": [ "string[] | undefined" @@ -4108,7 +4108,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 126 + "lineNumber": 127 }, "signature": [ "Map | undefined" @@ -4124,7 +4124,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 128 + "lineNumber": 129 }, "signature": [ "string | undefined" @@ -4140,7 +4140,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 132 + "lineNumber": 133 }, "signature": [ { @@ -4156,7 +4156,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 77 + "lineNumber": 78 }, "initialIsOpen": false }, @@ -4177,7 +4177,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 61 + "lineNumber": 62 } }, { @@ -4188,13 +4188,13 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 62 + "lineNumber": 63 } } ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 60 + "lineNumber": 61 }, "initialIsOpen": false }, @@ -5753,7 +5753,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 226 + "lineNumber": 227 }, "signature": [ "\"multiple\" | \"single\" | \"multiple-isolated\" | \"agnostic\"" @@ -8002,7 +8002,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 459 + "lineNumber": 462 }, "signature": [ { @@ -8024,7 +8024,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 461 + "lineNumber": 464 }, "signature": [ { @@ -8046,7 +8046,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 463 + "lineNumber": 466 }, "signature": [ { @@ -8068,7 +8068,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 465 + "lineNumber": 468 }, "signature": [ { @@ -8099,7 +8099,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 470 + "lineNumber": 473 }, "signature": [ { @@ -8121,7 +8121,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 472 + "lineNumber": 475 }, "signature": [ { @@ -8143,7 +8143,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 474 + "lineNumber": 477 }, "signature": [ { @@ -8165,7 +8165,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 476 + "lineNumber": 479 }, "signature": [ { @@ -8187,7 +8187,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 478 + "lineNumber": 481 }, "signature": [ { @@ -8209,7 +8209,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 480 + "lineNumber": 483 }, "signature": [ { @@ -8231,7 +8231,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 482 + "lineNumber": 485 }, "signature": [ { @@ -8247,7 +8247,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 457 + "lineNumber": 460 }, "initialIsOpen": false }, @@ -8272,7 +8272,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 505 + "lineNumber": 508 }, "signature": [ { @@ -8294,7 +8294,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 507 + "lineNumber": 510 }, "signature": [ { @@ -8316,7 +8316,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 509 + "lineNumber": 512 }, "signature": [ { @@ -8338,7 +8338,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 511 + "lineNumber": 514 }, "signature": [ { @@ -8360,7 +8360,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 513 + "lineNumber": 516 }, "signature": [ { @@ -8382,7 +8382,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 515 + "lineNumber": 518 }, "signature": [ { @@ -8397,7 +8397,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 503 + "lineNumber": 506 }, "initialIsOpen": false }, @@ -14272,7 +14272,7 @@ "description": [], "source": { "path": "src/core/server/index.ts", - "lineNumber": 425 + "lineNumber": 428 }, "signature": [ "{ savedObjects: { client: Pick<", @@ -14283,7 +14283,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">; typeRegistry: Pick<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">; typeRegistry: Pick<", { "pluginId": "core", "scope": "server", @@ -14307,7 +14307,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">; getExporter: (client: Pick<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">; getExporter: (client: Pick<", { "pluginId": "core", "scope": "server", @@ -14320,7 +14320,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 424 + "lineNumber": 427 }, "initialIsOpen": false }, @@ -15689,7 +15689,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">) => ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">) => ", { "pluginId": "core", "scope": "server", @@ -15715,7 +15715,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "description": [], "source": { @@ -15965,7 +15965,7 @@ "lineNumber": 22 }, "signature": [ - "Pick & { transport: { request(params: TransportRequestParams, options?: TransportRequestOptions | undefined): TransportRequestPromise; }; }" + "Pick & { transport: { request(params: TransportRequestParams, options?: TransportRequestOptions | undefined): TransportRequestPromise; }; }" ], "initialIsOpen": false }, @@ -16616,7 +16616,7 @@ ], "source": { "path": "src/core/server/index.ts", - "lineNumber": 493 + "lineNumber": 496 }, "signature": [ "() => Promise<[", diff --git a/api_docs/core_http.json b/api_docs/core_http.json index 8053550cc0e80..ce5ceb2840ec7 100644 --- a/api_docs/core_http.json +++ b/api_docs/core_http.json @@ -974,7 +974,7 @@ "lineNumber": 197 }, "signature": [ - "\"error\" | \"manual\" | \"follow\" | undefined" + "\"error\" | \"follow\" | \"manual\" | undefined" ] }, { diff --git a/api_docs/core_saved_objects.json b/api_docs/core_saved_objects.json index d862df7ef10bb..54f13f3911be6 100644 --- a/api_docs/core_saved_objects.json +++ b/api_docs/core_saved_objects.json @@ -1530,7 +1530,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 397 + "lineNumber": 402 }, "signature": [ "typeof ", @@ -1551,7 +1551,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 398 + "lineNumber": 403 }, "signature": [ "typeof ", @@ -1601,7 +1601,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 414 + "lineNumber": 419 } }, { @@ -1614,7 +1614,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 414 + "lineNumber": 419 } }, { @@ -1634,7 +1634,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 414 + "lineNumber": 419 } } ], @@ -1642,7 +1642,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 414 + "lineNumber": 419 } }, { @@ -1697,7 +1697,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 425 + "lineNumber": 430 } }, { @@ -1717,7 +1717,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 426 + "lineNumber": 431 } } ], @@ -1725,7 +1725,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 424 + "lineNumber": 429 } }, { @@ -1780,7 +1780,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 439 + "lineNumber": 444 } }, { @@ -1799,7 +1799,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 440 + "lineNumber": 445 } } ], @@ -1807,7 +1807,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 438 + "lineNumber": 443 } }, { @@ -1839,7 +1839,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 452 + "lineNumber": 457 } }, { @@ -1852,7 +1852,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 452 + "lineNumber": 457 } }, { @@ -1871,7 +1871,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 452 + "lineNumber": 457 } } ], @@ -1879,7 +1879,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 452 + "lineNumber": 457 } }, { @@ -1925,7 +1925,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 461 + "lineNumber": 466 } } ], @@ -1933,7 +1933,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 461 + "lineNumber": 466 } }, { @@ -1990,7 +1990,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 477 + "lineNumber": 482 } }, { @@ -2009,7 +2009,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 478 + "lineNumber": 483 } } ], @@ -2017,7 +2017,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 476 + "lineNumber": 481 } }, { @@ -2059,7 +2059,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 491 + "lineNumber": 496 } }, { @@ -2074,7 +2074,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 492 + "lineNumber": 497 } }, { @@ -2093,7 +2093,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 493 + "lineNumber": 498 } } ], @@ -2101,7 +2101,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 490 + "lineNumber": 495 } }, { @@ -2143,7 +2143,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 506 + "lineNumber": 511 } }, { @@ -2158,7 +2158,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 507 + "lineNumber": 512 } }, { @@ -2177,7 +2177,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 508 + "lineNumber": 513 } } ], @@ -2185,7 +2185,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 505 + "lineNumber": 510 } }, { @@ -2225,7 +2225,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 521 + "lineNumber": 526 } }, { @@ -2238,7 +2238,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 522 + "lineNumber": 527 } }, { @@ -2251,7 +2251,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 523 + "lineNumber": 528 } }, { @@ -2270,7 +2270,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 524 + "lineNumber": 529 } } ], @@ -2278,7 +2278,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 520 + "lineNumber": 525 } }, { @@ -2318,7 +2318,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 538 + "lineNumber": 543 } }, { @@ -2331,7 +2331,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 539 + "lineNumber": 544 } }, { @@ -2344,7 +2344,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 540 + "lineNumber": 545 } }, { @@ -2363,7 +2363,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 541 + "lineNumber": 546 } } ], @@ -2371,7 +2371,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 537 + "lineNumber": 542 } }, { @@ -2411,7 +2411,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 555 + "lineNumber": 560 } }, { @@ -2424,7 +2424,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 556 + "lineNumber": 561 } }, { @@ -2437,7 +2437,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 557 + "lineNumber": 562 } }, { @@ -2456,7 +2456,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 558 + "lineNumber": 563 } } ], @@ -2464,7 +2464,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 554 + "lineNumber": 559 } }, { @@ -2519,7 +2519,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 569 + "lineNumber": 574 } }, { @@ -2539,7 +2539,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 570 + "lineNumber": 575 } } ], @@ -2547,7 +2547,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 568 + "lineNumber": 573 } }, { @@ -2587,7 +2587,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 579 + "lineNumber": 584 } }, { @@ -2600,7 +2600,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 580 + "lineNumber": 585 } }, { @@ -2620,7 +2620,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 581 + "lineNumber": 586 } } ], @@ -2628,7 +2628,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 578 + "lineNumber": 583 } }, { @@ -2655,7 +2655,7 @@ ">" ], "description": [ - "\nOpens a Point In Time (PIT) against the indices for the specified Saved Object types.\nThe returned `id` can then be passed to {@link SavedObjectsClient.find} to search\nagainst that PIT." + "\nOpens a Point In Time (PIT) against the indices for the specified Saved Object types.\nThe returned `id` can then be passed to {@link SavedObjectsClient.find} to search\nagainst that PIT.\n\nOnly use this API if you have an advanced use case that's not solved by the\n{@link SavedObjectsClient.createPointInTimeFinder} method." ], "children": [ { @@ -2668,7 +2668,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 592 + "lineNumber": 600 } }, { @@ -2687,7 +2687,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 593 + "lineNumber": 601 } } ], @@ -2695,7 +2695,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 591 + "lineNumber": 599 } }, { @@ -2722,7 +2722,7 @@ ">" ], "description": [ - "\nCloses a Point In Time (PIT) by ID. This simply proxies the request to ES via the\nElasticsearch client, and is included in the Saved Objects Client as a convenience\nfor consumers who are using {@link SavedObjectsClient.openPointInTimeForType}." + "\nCloses a Point In Time (PIT) by ID. This simply proxies the request to ES via the\nElasticsearch client, and is included in the Saved Objects Client as a convenience\nfor consumers who are using {@link SavedObjectsClient.openPointInTimeForType}.\n\nOnly use this API if you have an advanced use case that's not solved by the\n{@link SavedObjectsClient.createPointInTimeFinder} method." ], "children": [ { @@ -2735,7 +2735,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 603 + "lineNumber": 614 } }, { @@ -2755,7 +2755,90 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 603 + "lineNumber": 614 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/core/server/saved_objects/service/saved_objects_client.ts", + "lineNumber": 614 + } + }, + { + "id": "def-server.SavedObjectsClient.createPointInTimeFinder", + "type": "Function", + "label": "createPointInTimeFinder", + "signature": [ + "(findOptions: Pick<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptions", + "text": "SavedObjectsFindOptions" + }, + ", \"type\" | \"filter\" | \"fields\" | \"search\" | \"perPage\" | \"sortField\" | \"sortOrder\" | \"searchFields\" | \"rootSearchFields\" | \"hasReference\" | \"hasReferenceOperator\" | \"defaultSearchOperator\" | \"namespaces\" | \"typeToNamespacesMap\" | \"preference\">, dependencies?: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsCreatePointInTimeFinderDependencies", + "text": "SavedObjectsCreatePointInTimeFinderDependencies" + }, + " | undefined) => ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.ISavedObjectsPointInTimeFinder", + "text": "ISavedObjectsPointInTimeFinder" + } + ], + "description": [ + "\nReturns a {@link ISavedObjectsPointInTimeFinder} to help page through\nlarge sets of saved objects. We strongly recommend using this API for\nany `find` queries that might return more than 1000 saved objects,\nhowever this API is only intended for use in server-side \"batch\"\nprocessing of objects where you are collecting all objects in memory\nor streaming them back to the client.\n\nDo NOT use this API in a route handler to facilitate paging through\nsaved objects on the client-side unless you are streaming all of the\nresults back to the client at once. Because the returned generator is\nstateful, you cannot rely on subsequent http requests retrieving new\npages from the same Kibana server in multi-instance deployments.\n\nThe generator wraps calls to {@link SavedObjectsClient.find} and iterates\nover multiple pages of results using `_pit` and `search_after`. This will\nopen a new Point-In-Time (PIT), and continue paging until a set of\nresults is received that's smaller than the designated `perPage`.\n\nOnce you have retrieved all of the results you need, it is recommended\nto call `close()` to clean up the PIT and prevent Elasticsearch from\nconsuming resources unnecessarily. This is only required if you are\ndone iterating and have not yet paged through all of the results: the\nPIT will automatically be closed for you once you reach the last page\nof results, or if the underlying call to `find` fails for any reason.\n" + ], + "children": [ + { + "type": "Object", + "label": "findOptions", + "isRequired": true, + "signature": [ + "Pick<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptions", + "text": "SavedObjectsFindOptions" + }, + ", \"type\" | \"filter\" | \"fields\" | \"search\" | \"perPage\" | \"sortField\" | \"sortOrder\" | \"searchFields\" | \"rootSearchFields\" | \"hasReference\" | \"hasReferenceOperator\" | \"defaultSearchOperator\" | \"namespaces\" | \"typeToNamespacesMap\" | \"preference\">" + ], + "description": [], + "source": { + "path": "src/core/server/saved_objects/service/saved_objects_client.ts", + "lineNumber": 664 + } + }, + { + "type": "Object", + "label": "dependencies", + "isRequired": false, + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsCreatePointInTimeFinderDependencies", + "text": "SavedObjectsCreatePointInTimeFinderDependencies" + }, + " | undefined" + ], + "description": [], + "source": { + "path": "src/core/server/saved_objects/service/saved_objects_client.ts", + "lineNumber": 665 } } ], @@ -2763,13 +2846,13 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 603 + "lineNumber": 663 } } ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 396 + "lineNumber": 401 }, "initialIsOpen": false }, @@ -4167,7 +4250,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 38 + "lineNumber": 37 }, "signature": [ "Pick<", @@ -4178,7 +4261,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -4189,7 +4272,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 39 + "lineNumber": 38 }, "signature": [ "Record" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -4274,7 +4357,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 50 + "lineNumber": 49 }, "signature": [ "Pick<", @@ -4296,7 +4379,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 51 + "lineNumber": 50 } }, { @@ -4307,7 +4390,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 52 + "lineNumber": 51 }, "signature": [ "Logger" @@ -4316,7 +4399,7 @@ ], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 48 + "lineNumber": 47 } } ], @@ -4324,7 +4407,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 43 + "lineNumber": 42 } }, { @@ -4364,7 +4447,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 75 + "lineNumber": 74 } } ], @@ -4374,7 +4457,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 75 + "lineNumber": 74 } }, { @@ -4414,7 +4497,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 93 + "lineNumber": 92 } } ], @@ -4424,13 +4507,13 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 93 + "lineNumber": 92 } } ], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 37 + "lineNumber": 36 }, "initialIsOpen": false }, @@ -4727,7 +4810,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -4820,7 +4903,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -5258,7 +5341,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 237 + "lineNumber": 257 } }, { @@ -5271,7 +5354,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 238 + "lineNumber": 258 } }, { @@ -5290,7 +5373,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 239 + "lineNumber": 259 } } ], @@ -5302,7 +5385,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 236 + "lineNumber": 256 } }, { @@ -5359,7 +5442,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 330 + "lineNumber": 350 } }, { @@ -5378,7 +5461,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 331 + "lineNumber": 351 } } ], @@ -5390,7 +5473,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 329 + "lineNumber": 349 } }, { @@ -5445,7 +5528,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 517 + "lineNumber": 541 } }, { @@ -5464,7 +5547,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 518 + "lineNumber": 542 } } ], @@ -5472,7 +5555,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 516 + "lineNumber": 540 } }, { @@ -5504,7 +5587,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 602 + "lineNumber": 627 } }, { @@ -5517,7 +5600,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 602 + "lineNumber": 627 } }, { @@ -5536,7 +5619,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 602 + "lineNumber": 627 } } ], @@ -5546,7 +5629,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 602 + "lineNumber": 627 } }, { @@ -5578,7 +5661,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 664 + "lineNumber": 690 } }, { @@ -5597,7 +5680,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 665 + "lineNumber": 691 } } ], @@ -5607,7 +5690,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 663 + "lineNumber": 689 } }, { @@ -5651,7 +5734,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 725 + "lineNumber": 751 } } ], @@ -5663,7 +5746,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 725 + "lineNumber": 751 } }, { @@ -5720,7 +5803,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 875 + "lineNumber": 906 } }, { @@ -5739,7 +5822,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 876 + "lineNumber": 907 } } ], @@ -5751,7 +5834,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 874 + "lineNumber": 905 } }, { @@ -5791,7 +5874,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 960 + "lineNumber": 993 } }, { @@ -5804,7 +5887,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 961 + "lineNumber": 994 } }, { @@ -5823,7 +5906,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 962 + "lineNumber": 995 } } ], @@ -5835,7 +5918,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 959 + "lineNumber": 992 } }, { @@ -5875,7 +5958,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 998 + "lineNumber": 1035 } }, { @@ -5888,7 +5971,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 999 + "lineNumber": 1036 } }, { @@ -5907,7 +5990,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1000 + "lineNumber": 1037 } } ], @@ -5919,7 +6002,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 997 + "lineNumber": 1034 } }, { @@ -5959,7 +6042,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1117 + "lineNumber": 1160 } }, { @@ -5972,7 +6055,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1118 + "lineNumber": 1161 } }, { @@ -5985,7 +6068,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1119 + "lineNumber": 1162 } }, { @@ -6004,7 +6087,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1120 + "lineNumber": 1163 } } ], @@ -6014,7 +6097,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1116 + "lineNumber": 1159 } }, { @@ -6054,7 +6137,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1190 + "lineNumber": 1232 } }, { @@ -6067,7 +6150,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1191 + "lineNumber": 1233 } }, { @@ -6080,7 +6163,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1192 + "lineNumber": 1234 } }, { @@ -6099,7 +6182,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1193 + "lineNumber": 1235 } } ], @@ -6107,7 +6190,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1189 + "lineNumber": 1231 } }, { @@ -6147,7 +6230,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1253 + "lineNumber": 1295 } }, { @@ -6160,7 +6243,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1254 + "lineNumber": 1296 } }, { @@ -6173,7 +6256,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1255 + "lineNumber": 1297 } }, { @@ -6192,7 +6275,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1256 + "lineNumber": 1298 } } ], @@ -6200,7 +6283,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1252 + "lineNumber": 1294 } }, { @@ -6257,7 +6340,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1358 + "lineNumber": 1401 } }, { @@ -6276,7 +6359,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1359 + "lineNumber": 1402 } } ], @@ -6288,7 +6371,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1357 + "lineNumber": 1400 } }, { @@ -6328,7 +6411,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1573 + "lineNumber": 1620 } }, { @@ -6341,7 +6424,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1574 + "lineNumber": 1621 } }, { @@ -6360,7 +6443,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1575 + "lineNumber": 1622 } } ], @@ -6368,7 +6451,7 @@ "returnComment": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1572 + "lineNumber": 1619 } }, { @@ -6392,7 +6475,7 @@ "section": "def-server.SavedObjectsIncrementCounterOptions", "text": "SavedObjectsIncrementCounterOptions" }, - ") => Promise<", + ") => Promise<", { "pluginId": "core", "scope": "common", @@ -6418,7 +6501,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1670 + "lineNumber": 1731 } }, { @@ -6433,7 +6516,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1671 + "lineNumber": 1732 } }, { @@ -6456,7 +6539,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1672 + "lineNumber": 1733 } }, { @@ -6470,14 +6553,15 @@ "docId": "kibCoreSavedObjectsPluginApi", "section": "def-server.SavedObjectsIncrementCounterOptions", "text": "SavedObjectsIncrementCounterOptions" - } + }, + "" ], "description": [ "- {@link SavedObjectsIncrementCounterOptions}" ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1673 + "lineNumber": 1734 } } ], @@ -6487,7 +6571,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1669 + "lineNumber": 1730 } }, { @@ -6514,7 +6598,7 @@ ">" ], "description": [ - "\nOpens a Point In Time (PIT) against the indices for the specified Saved Object types.\nThe returned `id` can then be passed to `SavedObjects.find` to search against that PIT.\n" + "\nOpens a Point In Time (PIT) against the indices for the specified Saved Object types.\nThe returned `id` can then be passed to `SavedObjects.find` to search against that PIT.\n\nOnly use this API if you have an advanced use case that's not solved by the\n{@link SavedObjectsRepository.createPointInTimeFinder} method.\n" ], "children": [ { @@ -6527,7 +6611,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1821 + "lineNumber": 1891 } }, { @@ -6546,7 +6630,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1822 + "lineNumber": 1892 } } ], @@ -6558,7 +6642,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1820 + "lineNumber": 1890 } }, { @@ -6585,7 +6669,7 @@ ">" ], "description": [ - "\nCloses a Point In Time (PIT) by ID. This simply proxies the request to ES\nvia the Elasticsearch client, and is included in the Saved Objects Client\nas a convenience for consumers who are using `openPointInTimeForType`.\n" + "\nCloses a Point In Time (PIT) by ID. This simply proxies the request to ES\nvia the Elasticsearch client, and is included in the Saved Objects Client\nas a convenience for consumers who are using `openPointInTimeForType`.\n\nOnly use this API if you have an advanced use case that's not solved by the\n{@link SavedObjectsRepository.createPointInTimeFinder} method.\n" ], "children": [ { @@ -6598,7 +6682,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1890 + "lineNumber": 1967 } }, { @@ -6620,7 +6704,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1891 + "lineNumber": 1968 } } ], @@ -6630,13 +6714,96 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 1889 + "lineNumber": 1966 + } + }, + { + "id": "def-server.SavedObjectsRepository.createPointInTimeFinder", + "type": "Function", + "label": "createPointInTimeFinder", + "signature": [ + "(findOptions: Pick<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptions", + "text": "SavedObjectsFindOptions" + }, + ", \"type\" | \"filter\" | \"fields\" | \"search\" | \"perPage\" | \"sortField\" | \"sortOrder\" | \"searchFields\" | \"rootSearchFields\" | \"hasReference\" | \"hasReferenceOperator\" | \"defaultSearchOperator\" | \"namespaces\" | \"typeToNamespacesMap\" | \"preference\">, dependencies?: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsCreatePointInTimeFinderDependencies", + "text": "SavedObjectsCreatePointInTimeFinderDependencies" + }, + " | undefined) => ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.ISavedObjectsPointInTimeFinder", + "text": "ISavedObjectsPointInTimeFinder" + } + ], + "description": [ + "\nReturns a {@link ISavedObjectsPointInTimeFinder} to help page through\nlarge sets of saved objects. We strongly recommend using this API for\nany `find` queries that might return more than 1000 saved objects,\nhowever this API is only intended for use in server-side \"batch\"\nprocessing of objects where you are collecting all objects in memory\nor streaming them back to the client.\n\nDo NOT use this API in a route handler to facilitate paging through\nsaved objects on the client-side unless you are streaming all of the\nresults back to the client at once. Because the returned generator is\nstateful, you cannot rely on subsequent http requests retrieving new\npages from the same Kibana server in multi-instance deployments.\n\nThis generator wraps calls to {@link SavedObjectsRepository.find} and\niterates over multiple pages of results using `_pit` and `search_after`.\nThis will open a new Point-In-Time (PIT), and continue paging until a\nset of results is received that's smaller than the designated `perPage`.\n\nOnce you have retrieved all of the results you need, it is recommended\nto call `close()` to clean up the PIT and prevent Elasticsearch from\nconsuming resources unnecessarily. This is only required if you are\ndone iterating and have not yet paged through all of the results: the\nPIT will automatically be closed for you once you reach the last page\nof results, or if the underlying call to `find` fails for any reason.\n" + ], + "children": [ + { + "type": "Object", + "label": "findOptions", + "isRequired": true, + "signature": [ + "Pick<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptions", + "text": "SavedObjectsFindOptions" + }, + ", \"type\" | \"filter\" | \"fields\" | \"search\" | \"perPage\" | \"sortField\" | \"sortOrder\" | \"searchFields\" | \"rootSearchFields\" | \"hasReference\" | \"hasReferenceOperator\" | \"defaultSearchOperator\" | \"namespaces\" | \"typeToNamespacesMap\" | \"preference\">" + ], + "description": [], + "source": { + "path": "src/core/server/saved_objects/service/lib/repository.ts", + "lineNumber": 2023 + } + }, + { + "type": "Object", + "label": "dependencies", + "isRequired": false, + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsCreatePointInTimeFinderDependencies", + "text": "SavedObjectsCreatePointInTimeFinderDependencies" + }, + " | undefined" + ], + "description": [], + "source": { + "path": "src/core/server/saved_objects/service/lib/repository.ts", + "lineNumber": 2024 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/core/server/saved_objects/service/lib/repository.ts", + "lineNumber": 2022 } } ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 143 + "lineNumber": 158 }, "initialIsOpen": false }, @@ -7617,6 +7784,62 @@ ], "functions": [], "interfaces": [ + { + "id": "def-server.ISavedObjectsPointInTimeFinder", + "type": "Interface", + "label": "ISavedObjectsPointInTimeFinder", + "description": [], + "tags": [ + "public" + ], + "children": [ + { + "tags": [], + "id": "def-server.ISavedObjectsPointInTimeFinder.find", + "type": "Function", + "label": "find", + "description": [ + "\nAn async generator which wraps calls to `savedObjectsClient.find` and\niterates over multiple pages of results using `_pit` and `search_after`.\nThis will open a new Point-In-Time (PIT), and continue paging until a set\nof results is received that's smaller than the designated `perPage` size." + ], + "source": { + "path": "src/core/server/saved_objects/service/lib/point_in_time_finder.ts", + "lineNumber": 49 + }, + "signature": [ + "() => AsyncGenerator<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindResponse", + "text": "SavedObjectsFindResponse" + }, + ", any, unknown>" + ] + }, + { + "tags": [], + "id": "def-server.ISavedObjectsPointInTimeFinder.close", + "type": "Function", + "label": "close", + "description": [ + "\nCloses the Point-In-Time associated with this finder instance.\n\nOnce you have retrieved all of the results you need, it is recommended\nto call `close()` to clean up the PIT and prevent Elasticsearch from\nconsuming resources unnecessarily. This is only required if you are\ndone iterating and have not yet paged through all of the results: the\nPIT will automatically be closed for you once you reach the last page\nof results, or if the underlying call to `find` fails for any reason." + ], + "source": { + "path": "src/core/server/saved_objects/service/lib/point_in_time_finder.ts", + "lineNumber": 60 + }, + "signature": [ + "() => Promise" + ] + } + ], + "source": { + "path": "src/core/server/saved_objects/service/lib/point_in_time_finder.ts", + "lineNumber": 42 + }, + "initialIsOpen": false + }, { "id": "def-server.SavedObjectExportBaseOptions", "type": "Interface", @@ -7843,7 +8066,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 219 + "lineNumber": 224 }, "signature": [ "string | undefined" @@ -7859,7 +8082,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 221 + "lineNumber": 226 }, "signature": [ "boolean | \"wait_for\" | undefined" @@ -7868,7 +8091,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 217 + "lineNumber": 222 }, "initialIsOpen": false }, @@ -7893,7 +8116,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 230 + "lineNumber": 235 }, "signature": [ "string[]" @@ -7902,7 +8125,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 228 + "lineNumber": 233 }, "initialIsOpen": false }, @@ -7927,7 +8150,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 141 + "lineNumber": 142 }, "signature": [ "string | undefined" @@ -7936,7 +8159,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 139 + "lineNumber": 140 }, "initialIsOpen": false }, @@ -7969,7 +8192,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 66 + "lineNumber": 71 }, "signature": [ "string | undefined" @@ -7983,7 +8206,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 67 + "lineNumber": 72 } }, { @@ -7994,7 +8217,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 68 + "lineNumber": 73 }, "signature": [ "T" @@ -8008,7 +8231,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 69 + "lineNumber": 74 }, "signature": [ "string | undefined" @@ -8022,7 +8245,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 70 + "lineNumber": 75 }, "signature": [ { @@ -8045,7 +8268,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 72 + "lineNumber": 77 }, "signature": [ { @@ -8068,7 +8291,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 82 + "lineNumber": 87 }, "signature": [ "string | undefined" @@ -8084,7 +8307,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 84 + "lineNumber": 89 }, "signature": [ "string | undefined" @@ -8100,7 +8323,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 91 + "lineNumber": 96 }, "signature": [ "string[] | undefined" @@ -8109,7 +8332,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 65 + "lineNumber": 70 }, "initialIsOpen": false }, @@ -8132,7 +8355,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 294 + "lineNumber": 299 } }, { @@ -8143,7 +8366,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 295 + "lineNumber": 300 } }, { @@ -8156,7 +8379,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 297 + "lineNumber": 302 }, "signature": [ "string[] | undefined" @@ -8165,7 +8388,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 293 + "lineNumber": 298 }, "initialIsOpen": false }, @@ -8198,7 +8421,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 120 + "lineNumber": 125 }, "signature": [ { @@ -8214,7 +8437,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 119 + "lineNumber": 124 }, "initialIsOpen": false }, @@ -8247,7 +8470,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 305 + "lineNumber": 310 }, "signature": [ { @@ -8263,7 +8486,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 304 + "lineNumber": 309 }, "initialIsOpen": false }, @@ -8306,7 +8529,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 101 + "lineNumber": 106 } }, { @@ -8319,7 +8542,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 103 + "lineNumber": 108 } }, { @@ -8332,7 +8555,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 105 + "lineNumber": 110 }, "signature": [ "Partial" @@ -8348,7 +8571,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 112 + "lineNumber": 117 }, "signature": [ "string | undefined" @@ -8357,7 +8580,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 98 + "lineNumber": 103 }, "initialIsOpen": false }, @@ -8399,7 +8622,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 275 + "lineNumber": 280 }, "signature": [ "boolean | \"wait_for\" | undefined" @@ -8408,7 +8631,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 273 + "lineNumber": 278 }, "initialIsOpen": false }, @@ -8441,7 +8664,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 313 + "lineNumber": 318 }, "signature": [ { @@ -8457,7 +8680,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 312 + "lineNumber": 317 }, "initialIsOpen": false }, @@ -8480,7 +8703,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 184 + "lineNumber": 189 } }, { @@ -8491,13 +8714,13 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 185 + "lineNumber": 190 } } ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 183 + "lineNumber": 188 }, "initialIsOpen": false }, @@ -8520,7 +8743,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 193 + "lineNumber": 198 }, "signature": [ "{ id: string; type: string; error: ", @@ -8537,7 +8760,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 192 + "lineNumber": 197 }, "initialIsOpen": false }, @@ -8617,7 +8840,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -8689,7 +8912,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 385 + "lineNumber": 390 } }, { @@ -8702,13 +8925,13 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 389 + "lineNumber": 394 } } ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 380 + "lineNumber": 385 }, "initialIsOpen": false }, @@ -8931,7 +9154,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 27 + "lineNumber": 32 }, "signature": [ "string | undefined" @@ -8947,7 +9170,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 29 + "lineNumber": 34 }, "signature": [ "boolean | undefined" @@ -8963,7 +9186,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 34 + "lineNumber": 39 }, "signature": [ "string | undefined" @@ -8979,7 +9202,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 36 + "lineNumber": 41 }, "signature": [ { @@ -9002,7 +9225,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 46 + "lineNumber": 51 }, "signature": [ "string | undefined" @@ -9016,7 +9239,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 47 + "lineNumber": 52 }, "signature": [ { @@ -9039,7 +9262,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 49 + "lineNumber": 54 }, "signature": [ "boolean | \"wait_for\" | undefined" @@ -9055,7 +9278,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 51 + "lineNumber": 56 }, "signature": [ "string | undefined" @@ -9071,7 +9294,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 58 + "lineNumber": 63 }, "signature": [ "string[] | undefined" @@ -9080,7 +9303,45 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 25 + "lineNumber": 30 + }, + "initialIsOpen": false + }, + { + "id": "def-server.SavedObjectsCreatePointInTimeFinderDependencies", + "type": "Interface", + "label": "SavedObjectsCreatePointInTimeFinderDependencies", + "description": [], + "tags": [ + "public" + ], + "children": [ + { + "tags": [], + "id": "def-server.SavedObjectsCreatePointInTimeFinderDependencies.client", + "type": "Object", + "label": "client", + "description": [], + "source": { + "path": "src/core/server/saved_objects/service/lib/point_in_time_finder.ts", + "lineNumber": 30 + }, + "signature": [ + "Pick, \"find\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ] + } + ], + "source": { + "path": "src/core/server/saved_objects/service/lib/point_in_time_finder.ts", + "lineNumber": 29 }, "initialIsOpen": false }, @@ -9122,7 +9383,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 118 + "lineNumber": 133 }, "signature": [ "boolean | undefined" @@ -9131,7 +9392,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 116 + "lineNumber": 131 }, "initialIsOpen": false }, @@ -9173,7 +9434,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 239 + "lineNumber": 244 }, "signature": [ "boolean | \"wait_for\" | undefined" @@ -9182,7 +9443,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 237 + "lineNumber": 242 }, "initialIsOpen": false }, @@ -9207,7 +9468,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 248 + "lineNumber": 253 }, "signature": [ "string[]" @@ -9216,7 +9477,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 246 + "lineNumber": 251 }, "initialIsOpen": false }, @@ -9258,7 +9519,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 284 + "lineNumber": 289 }, "signature": [ "boolean | \"wait_for\" | undefined" @@ -9274,7 +9535,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 286 + "lineNumber": 291 }, "signature": [ "boolean | undefined" @@ -9283,7 +9544,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 282 + "lineNumber": 287 }, "initialIsOpen": false }, @@ -9548,7 +9809,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 78 + "lineNumber": 79 }, "signature": [ "string | string[]" @@ -9562,7 +9823,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 79 + "lineNumber": 80 }, "signature": [ "number | undefined" @@ -9576,7 +9837,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 80 + "lineNumber": 81 }, "signature": [ "number | undefined" @@ -9590,7 +9851,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 81 + "lineNumber": 82 }, "signature": [ "string | undefined" @@ -9599,15 +9860,15 @@ { "tags": [], "id": "def-server.SavedObjectsFindOptions.sortOrder", - "type": "string", + "type": "CompoundType", "label": "sortOrder", "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 82 + "lineNumber": 83 }, "signature": [ - "string | undefined" + "\"asc\" | \"desc\" | \"_doc\" | undefined" ] }, { @@ -9620,7 +9881,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 88 + "lineNumber": 89 }, "signature": [ "string[] | undefined" @@ -9636,7 +9897,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 90 + "lineNumber": 91 }, "signature": [ "string | undefined" @@ -9652,7 +9913,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 92 + "lineNumber": 93 }, "signature": [ "string[] | undefined" @@ -9668,10 +9929,10 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 96 + "lineNumber": 97 }, "signature": [ - "unknown[] | undefined" + "string[] | undefined" ] }, { @@ -9684,7 +9945,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 101 + "lineNumber": 102 }, "signature": [ "string[] | undefined" @@ -9700,7 +9961,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 107 + "lineNumber": 108 }, "signature": [ { @@ -9731,7 +9992,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 111 + "lineNumber": 112 }, "signature": [ "\"AND\" | \"OR\" | undefined" @@ -9747,7 +10008,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 116 + "lineNumber": 117 }, "signature": [ "\"AND\" | \"OR\" | undefined" @@ -9761,7 +10022,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 117 + "lineNumber": 118 }, "signature": [ "any" @@ -9775,7 +10036,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 118 + "lineNumber": 119 }, "signature": [ "string[] | undefined" @@ -9791,7 +10052,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 126 + "lineNumber": 127 }, "signature": [ "Map | undefined" @@ -9807,7 +10068,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 128 + "lineNumber": 129 }, "signature": [ "string | undefined" @@ -9823,7 +10084,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 132 + "lineNumber": 133 }, "signature": [ { @@ -9839,7 +10100,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 77 + "lineNumber": 78 }, "initialIsOpen": false }, @@ -9860,7 +10121,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 61 + "lineNumber": 62 } }, { @@ -9871,13 +10132,13 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 62 + "lineNumber": 63 } } ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 60 + "lineNumber": 61 }, "initialIsOpen": false }, @@ -9910,7 +10171,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 172 + "lineNumber": 177 }, "signature": [ { @@ -9931,7 +10192,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 173 + "lineNumber": 178 } }, { @@ -9942,7 +10203,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 174 + "lineNumber": 179 } }, { @@ -9953,7 +10214,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 175 + "lineNumber": 180 } }, { @@ -9964,7 +10225,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 176 + "lineNumber": 181 }, "signature": [ "string | undefined" @@ -9973,7 +10234,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 171 + "lineNumber": 176 }, "initialIsOpen": false }, @@ -10010,7 +10271,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 131 + "lineNumber": 136 } }, { @@ -10023,16 +10284,16 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 160 + "lineNumber": 165 }, "signature": [ - "unknown[] | undefined" + "string[] | undefined" ] } ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 127 + "lineNumber": 132 }, "initialIsOpen": false }, @@ -10971,7 +11232,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 135 + "lineNumber": 150 } }, { @@ -10984,7 +11245,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 137 + "lineNumber": 152 }, "signature": [ "number | undefined" @@ -10993,7 +11254,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 133 + "lineNumber": 148 }, "initialIsOpen": false }, @@ -11009,7 +11270,7 @@ "section": "def-server.SavedObjectsIncrementCounterOptions", "text": "SavedObjectsIncrementCounterOptions" }, - " extends ", + " extends ", { "pluginId": "core", "scope": "server", @@ -11033,7 +11294,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 102 + "lineNumber": 113 }, "signature": [ "boolean | undefined" @@ -11049,7 +11310,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 104 + "lineNumber": 115 }, "signature": [ { @@ -11072,16 +11333,32 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 109 + "lineNumber": 120 }, "signature": [ "boolean | \"wait_for\" | undefined" ] + }, + { + "tags": [], + "id": "def-server.SavedObjectsIncrementCounterOptions.upsertAttributes", + "type": "Uncategorized", + "label": "upsertAttributes", + "description": [ + "\nAttributes to use when upserting the document if it doesn't exist." + ], + "source": { + "path": "src/core/server/saved_objects/service/lib/repository.ts", + "lineNumber": 124 + }, + "signature": [ + "Attributes | undefined" + ] } ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 97 + "lineNumber": 107 }, "initialIsOpen": false }, @@ -11243,7 +11520,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 355 + "lineNumber": 360 }, "signature": [ "string | undefined" @@ -11259,7 +11536,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 359 + "lineNumber": 364 }, "signature": [ "string | undefined" @@ -11268,7 +11545,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 351 + "lineNumber": 356 }, "initialIsOpen": false }, @@ -11291,13 +11568,13 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 369 + "lineNumber": 374 } } ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 365 + "lineNumber": 370 }, "initialIsOpen": false }, @@ -11318,7 +11595,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 69 + "lineNumber": 70 } }, { @@ -11329,7 +11606,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 70 + "lineNumber": 71 }, "signature": [ "string | undefined" @@ -11338,7 +11615,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 68 + "lineNumber": 69 }, "initialIsOpen": false }, @@ -11491,7 +11768,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 257 + "lineNumber": 262 }, "signature": [ "boolean | undefined" @@ -11500,7 +11777,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 255 + "lineNumber": 260 }, "initialIsOpen": false }, @@ -11542,13 +11819,13 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 266 + "lineNumber": 271 } } ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 264 + "lineNumber": 269 }, "initialIsOpen": false }, @@ -11592,7 +11869,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"deleteByNamespace\" | \"incrementCounter\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\" | \"deleteByNamespace\" | \"incrementCounter\">" ] }, { @@ -11616,7 +11893,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"deleteByNamespace\" | \"incrementCounter\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\" | \"deleteByNamespace\" | \"incrementCounter\">" ] } ], @@ -11741,7 +12018,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 331 + "lineNumber": 336 }, "signature": [ { @@ -11764,7 +12041,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 341 + "lineNumber": 346 }, "signature": [ "\"conflict\" | \"exactMatch\" | \"aliasMatch\"" @@ -11780,7 +12057,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 345 + "lineNumber": 350 }, "signature": [ "string | undefined" @@ -11789,7 +12066,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 330 + "lineNumber": 335 }, "initialIsOpen": false }, @@ -11931,7 +12208,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -11963,7 +12240,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"deleteByNamespace\" | \"incrementCounter\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\" | \"deleteByNamespace\" | \"incrementCounter\">" ] }, { @@ -11987,7 +12264,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"deleteByNamespace\" | \"incrementCounter\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\" | \"deleteByNamespace\" | \"incrementCounter\">" ] }, { @@ -12034,7 +12311,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">) => Pick<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">) => Pick<", { "pluginId": "core", "scope": "server", @@ -12066,7 +12343,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">) => Pick<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">) => Pick<", { "pluginId": "core", "scope": "server", @@ -12127,7 +12404,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 50 + "lineNumber": 51 }, "signature": [ "{ [status: string]: number; skipped: number; migrated: number; }" @@ -12136,7 +12413,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 49 + "lineNumber": 50 }, "initialIsOpen": false }, @@ -12159,7 +12436,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 237 + "lineNumber": 238 } }, { @@ -12172,7 +12449,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 244 + "lineNumber": 245 } }, { @@ -12185,7 +12462,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 248 + "lineNumber": 249 }, "signature": [ { @@ -12207,7 +12484,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 252 + "lineNumber": 253 }, "signature": [ "string | undefined" @@ -12223,7 +12500,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 256 + "lineNumber": 257 }, "signature": [ "string | undefined" @@ -12239,7 +12516,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 260 + "lineNumber": 261 }, "signature": [ { @@ -12261,7 +12538,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 264 + "lineNumber": 265 }, "signature": [ { @@ -12292,7 +12569,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 313 + "lineNumber": 314 }, "signature": [ "string | undefined" @@ -12308,7 +12585,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 317 + "lineNumber": 318 }, "signature": [ { @@ -12324,7 +12601,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 233 + "lineNumber": 234 }, "initialIsOpen": false }, @@ -12349,7 +12626,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 329 + "lineNumber": 330 }, "signature": [ "boolean | undefined" @@ -12365,7 +12642,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 333 + "lineNumber": 334 }, "signature": [ "string | undefined" @@ -12381,7 +12658,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 338 + "lineNumber": 339 }, "signature": [ "string | undefined" @@ -12397,7 +12674,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 343 + "lineNumber": 344 }, "signature": [ "((savedObject: ", @@ -12421,7 +12698,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 348 + "lineNumber": 349 }, "signature": [ "((savedObject: ", @@ -12445,7 +12722,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 357 + "lineNumber": 358 }, "signature": [ "((savedObject: ", @@ -12469,7 +12746,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 368 + "lineNumber": 369 }, "signature": [ { @@ -12492,7 +12769,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 411 + "lineNumber": 412 }, "signature": [ { @@ -12508,7 +12785,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 325 + "lineNumber": 326 }, "initialIsOpen": false }, @@ -12606,7 +12883,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 206 + "lineNumber": 211 }, "signature": [ "string | undefined" @@ -12622,7 +12899,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 208 + "lineNumber": 213 }, "signature": [ { @@ -12645,7 +12922,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 210 + "lineNumber": 215 }, "signature": [ "boolean | \"wait_for\" | undefined" @@ -12654,7 +12931,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 204 + "lineNumber": 209 }, "initialIsOpen": false }, @@ -12689,7 +12966,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 322 + "lineNumber": 327 }, "signature": [ "Partial" @@ -12703,7 +12980,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 323 + "lineNumber": 328 }, "signature": [ { @@ -12719,7 +12996,7 @@ ], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 320 + "lineNumber": 325 }, "initialIsOpen": false } @@ -12736,7 +13013,7 @@ "description": [], "source": { "path": "src/core/server/saved_objects/export/saved_objects_exporter.ts", - "lineNumber": 32 + "lineNumber": 31 }, "signature": [ "{ exportByTypes: (options: SavedObjectsExportByTypeOptions) => Promise<", @@ -12776,10 +13053,10 @@ ], "source": { "path": "src/core/server/saved_objects/service/lib/repository.ts", - "lineNumber": 128 + "lineNumber": 143 }, "signature": [ - "{ get: (type: string, id: string, options?: SavedObjectsBaseOptions) => Promise>; delete: (type: string, id: string, options?: SavedObjectsDeleteOptions) => Promise<{}>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; find: (options: SavedObjectsFindOptions) => Promise>; update: (type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions) => Promise>; bulkCreate: (objects: SavedObjectsBulkCreateObject[], options?: SavedObjectsCreateOptions) => Promise>; bulkGet: (objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions) => Promise>; bulkUpdate: (objects: SavedObjectsBulkUpdateObject[], options?: SavedObjectsBulkUpdateOptions) => Promise>; checkConflicts: (objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions) => Promise; resolve: (type: string, id: string, options?: SavedObjectsBaseOptions) => Promise>; addToNamespaces: (type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions) => Promise; deleteFromNamespaces: (type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions) => Promise; removeReferencesTo: (type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions) => Promise; openPointInTimeForType: (type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions) => Promise; closePointInTime: (id: string, options?: SavedObjectsBaseOptions | undefined) => Promise; deleteByNamespace: (namespace: string, options?: SavedObjectsDeleteByNamespaceOptions) => Promise; incrementCounter: (type: string, id: string, counterFields: (string | SavedObjectsIncrementCounterField)[], options?: SavedObjectsIncrementCounterOptions) => Promise>; }" + "{ get: (type: string, id: string, options?: SavedObjectsBaseOptions) => Promise>; delete: (type: string, id: string, options?: SavedObjectsDeleteOptions) => Promise<{}>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; find: (options: SavedObjectsFindOptions) => Promise>; update: (type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions) => Promise>; bulkCreate: (objects: SavedObjectsBulkCreateObject[], options?: SavedObjectsCreateOptions) => Promise>; bulkGet: (objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions) => Promise>; bulkUpdate: (objects: SavedObjectsBulkUpdateObject[], options?: SavedObjectsBulkUpdateOptions) => Promise>; checkConflicts: (objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions) => Promise; resolve: (type: string, id: string, options?: SavedObjectsBaseOptions) => Promise>; addToNamespaces: (type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions) => Promise; deleteFromNamespaces: (type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions) => Promise; removeReferencesTo: (type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions) => Promise; openPointInTimeForType: (type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions) => Promise; closePointInTime: (id: string, options?: SavedObjectsBaseOptions | undefined) => Promise; createPointInTimeFinder: (findOptions: Pick, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies | undefined) => ISavedObjectsPointInTimeFinder; deleteByNamespace: (namespace: string, options?: SavedObjectsDeleteByNamespaceOptions) => Promise; incrementCounter: (type: string, id: string, counterFields: (string | SavedObjectsIncrementCounterField)[], options?: SavedObjectsIncrementCounterOptions) => Promise>; }" ], "initialIsOpen": false }, @@ -12814,7 +13091,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 148 + "lineNumber": 149 }, "signature": [ "false | true | \"wait_for\"" @@ -12895,7 +13172,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 212 + "lineNumber": 213 }, "signature": [ "{ get: (type: string, id: string, options?: SavedObjectsBaseOptions) => Promise>; delete: (type: string, id: string, options?: ", @@ -12972,7 +13249,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "initialIsOpen": false }, @@ -13041,7 +13318,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "initialIsOpen": false }, @@ -13055,13 +13332,46 @@ "description": [], "source": { "path": "src/core/server/saved_objects/service/saved_objects_client.ts", - "lineNumber": 375 + "lineNumber": 380 }, "signature": [ "SavedObjectsBaseOptions" ], "initialIsOpen": false }, + { + "id": "def-server.SavedObjectsCreatePointInTimeFinderOptions", + "type": "Type", + "label": "SavedObjectsCreatePointInTimeFinderOptions", + "tags": [ + "public" + ], + "description": [], + "source": { + "path": "src/core/server/saved_objects/service/lib/point_in_time_finder.ts", + "lineNumber": 21 + }, + "signature": [ + "{ type: string | string[]; filter?: any; fields?: string[] | undefined; search?: string | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: \"asc\" | \"desc\" | \"_doc\" | undefined; searchFields?: string[] | undefined; rootSearchFields?: string[] | undefined; hasReference?: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + "[] | undefined; hasReferenceOperator?: \"AND\" | \"OR\" | undefined; defaultSearchOperator?: \"AND\" | \"OR\" | undefined; namespaces?: string[] | undefined; typeToNamespacesMap?: Map | undefined; preference?: string | undefined; }" + ], + "initialIsOpen": false + }, { "id": "def-server.SavedObjectsExportTransform", "type": "Type", @@ -13210,7 +13520,7 @@ ], "source": { "path": "src/core/server/saved_objects/types.ts", - "lineNumber": 226 + "lineNumber": 227 }, "signature": [ "\"multiple\" | \"single\" | \"multiple-isolated\" | \"agnostic\"" diff --git a/api_docs/data.json b/api_docs/data.json index a9ef03d881ce8..a51ad465fe903 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -7022,8 +7022,7 @@ "docId": "kibDataSearchPluginApi", "section": "def-common.ISearchRequestParams", "text": "ISearchRequestParams" - }, - ">" + } ], "description": [], "children": [ @@ -8696,7 +8695,7 @@ "section": "def-common.ISearchRequestParams", "text": "ISearchRequestParams" }, - ">>" + ">" ], "description": [], "tags": [], @@ -8709,7 +8708,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 20 + "lineNumber": 19 }, "signature": [ "string | undefined" @@ -8718,7 +8717,7 @@ ], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 19 + "lineNumber": 18 }, "initialIsOpen": false }, @@ -10763,7 +10762,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 37 + "lineNumber": 40 } }, { @@ -10774,7 +10773,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 38 + "lineNumber": 41 } }, { @@ -10785,7 +10784,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 39 + "lineNumber": 42 } }, { @@ -10796,7 +10795,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 40 + "lineNumber": 43 } }, { @@ -10807,7 +10806,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 41 + "lineNumber": 44 } }, { @@ -10818,13 +10817,13 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 42 + "lineNumber": 45 } } ], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 36 + "lineNumber": 39 }, "initialIsOpen": false }, @@ -11484,7 +11483,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 13 + "lineNumber": 12 }, "signature": [ "\"es\"" @@ -11608,7 +11607,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 26 + "lineNumber": 34 }, "signature": [ "ExpressionFunctionDefinition<\"kibana_context\", ", @@ -11751,10 +11750,10 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 23 + "lineNumber": 22 }, "signature": [ - "IKibanaSearchResponse>" + "IKibanaSearchResponse>" ], "initialIsOpen": false }, @@ -11847,7 +11846,7 @@ "description": [], "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 34 + "lineNumber": 35 }, "signature": [ "ExpressionFunctionDefinition<\"indexPatternLoad\", null, Arguments, Output, ", @@ -14487,15 +14486,7 @@ "lineNumber": 46 }, "signature": [ - "{ addQuerySuggestionProvider: (language: string, provider: ", - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataAutocompletePluginApi", - "section": "def-public.QuerySuggestionGetFn", - "text": "QuerySuggestionGetFn" - }, - ") => void; getQuerySuggestions: ", + "{ getQuerySuggestions: ", { "pluginId": "data", "scope": "public", @@ -15163,7 +15154,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, elasticsearchClient: ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, elasticsearchClient: ", { "pluginId": "core", "scope": "server", @@ -15190,7 +15181,7 @@ "description": [], "source": { "path": "src/plugins/data/server/plugin.ts", - "lineNumber": 107 + "lineNumber": 104 } } ], @@ -15198,7 +15189,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/server/plugin.ts", - "lineNumber": 107 + "lineNumber": 104 } }, { @@ -15214,7 +15205,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/server/plugin.ts", - "lineNumber": 121 + "lineNumber": 118 } } ], @@ -19507,7 +19498,7 @@ "section": "def-common.ISearchRequestParams", "text": "ISearchRequestParams" }, - ">>" + ">" ], "description": [], "tags": [], @@ -19520,7 +19511,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 20 + "lineNumber": 19 }, "signature": [ "string | undefined" @@ -19529,7 +19520,7 @@ ], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 19 + "lineNumber": 18 }, "initialIsOpen": false }, @@ -20433,7 +20424,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 13 + "lineNumber": 12 }, "signature": [ "\"es\"" @@ -20527,7 +20518,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 26 + "lineNumber": 34 }, "signature": [ "ExpressionFunctionDefinition<\"kibana_context\", ", @@ -20636,10 +20627,10 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 23 + "lineNumber": 22 }, "signature": [ - "IKibanaSearchResponse>" + "IKibanaSearchResponse>" ], "initialIsOpen": false }, @@ -20717,7 +20708,7 @@ "description": [], "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 34 + "lineNumber": 35 }, "signature": [ "ExpressionFunctionDefinition<\"indexPatternLoad\", null, Arguments, Output, ", diff --git a/api_docs/data_autocomplete.json b/api_docs/data_autocomplete.json index a66ca49d0c181..7792a7abe5c8a 100644 --- a/api_docs/data_autocomplete.json +++ b/api_docs/data_autocomplete.json @@ -324,7 +324,7 @@ "description": [], "source": { "path": "src/plugins/data/public/autocomplete/autocomplete_service.ts", - "lineNumber": 93 + "lineNumber": 96 }, "signature": [ "{ getQuerySuggestions: QuerySuggestionGetFn; hasQuerySuggestions: (language: string) => boolean; getValueSuggestions: ValueSuggestionsGetFn; }" diff --git a/api_docs/data_enhanced.json b/api_docs/data_enhanced.json index 5bd7a970f9b73..80f1d1fc15a5a 100644 --- a/api_docs/data_enhanced.json +++ b/api_docs/data_enhanced.json @@ -46,7 +46,7 @@ "description": [], "source": { "path": "x-pack/plugins/data_enhanced/public/plugin.ts", - "lineNumber": 40 + "lineNumber": 38 }, "signature": [ "void" @@ -903,9 +903,7 @@ "lineNumber": 27 }, "signature": [ - "IKibanaSearchResponse>" + "IKibanaSearchResponse>" ], "initialIsOpen": false }, diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index afcea7d50b304..8058f6a72f9c3 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -368,7 +368,7 @@ "section": "def-server.DataPluginStart", "text": "DataPluginStart" }, - ">, { logger, expressions }: ", + ">, { expressions }: ", { "pluginId": "data", "scope": "server", @@ -413,12 +413,12 @@ "description": [], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 49 + "lineNumber": 47 } }, { "type": "Object", - "label": "{ logger, expressions }", + "label": "{ expressions }", "isRequired": true, "signature": [ { @@ -432,7 +432,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 50 + "lineNumber": 48 } } ], @@ -440,7 +440,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 48 + "lineNumber": 46 } }, { @@ -472,7 +472,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, elasticsearchClient: ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, elasticsearchClient: ", { "pluginId": "core", "scope": "server", @@ -507,7 +507,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 76 + "lineNumber": 58 } }, { @@ -526,7 +526,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 76 + "lineNumber": 58 } } ], @@ -534,13 +534,13 @@ "returnComment": [], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 76 + "lineNumber": 58 } } ], "source": { "path": "src/plugins/data/server/index_patterns/index_patterns_service.ts", - "lineNumber": 47 + "lineNumber": 45 }, "initialIsOpen": false } @@ -3903,7 +3903,7 @@ "label": "getIndexPatternLoadMeta", "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 41 + "lineNumber": 42 }, "tags": [], "returnComment": [], @@ -5937,7 +5937,7 @@ "description": [], "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 18 + "lineNumber": 19 }, "signature": [ "\"index_pattern\"" @@ -5951,7 +5951,7 @@ "description": [], "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 19 + "lineNumber": 20 }, "signature": [ { @@ -5966,7 +5966,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 17 + "lineNumber": 18 }, "initialIsOpen": false }, @@ -6677,7 +6677,7 @@ "description": [], "source": { "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", - "lineNumber": 34 + "lineNumber": 35 }, "signature": [ "ExpressionFunctionDefinition<\"indexPatternLoad\", null, Arguments, Output, ", diff --git a/api_docs/data_search.json b/api_docs/data_search.json index a75b669cbd288..53b8294de02fc 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -1621,7 +1621,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 88 + "lineNumber": 90 }, "signature": [ "(sessionId: string, attributes: Partial) => Promise<", @@ -1643,7 +1643,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 89 + "lineNumber": 91 }, "signature": [ "(sessionId: string) => Promise<", @@ -1665,7 +1665,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 90 + "lineNumber": 92 }, "signature": [ "(options: Pick<", @@ -1695,7 +1695,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 91 + "lineNumber": 93 }, "signature": [ "(sessionId: string, attributes: Partial) => Promise<", @@ -1717,7 +1717,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 92 + "lineNumber": 94 }, "signature": [ "(sessionId: string) => Promise<{}>" @@ -1731,7 +1731,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 93 + "lineNumber": 95 }, "signature": [ "(sessionId: string) => Promise<{}>" @@ -1745,7 +1745,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 94 + "lineNumber": 96 }, "signature": [ "(sessionId: string, expires: Date) => Promise<", @@ -1762,7 +1762,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 87 + "lineNumber": 89 }, "initialIsOpen": false }, @@ -1843,7 +1843,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 41 + "lineNumber": 43 }, "signature": [ { @@ -1865,7 +1865,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 46 + "lineNumber": 48 }, "signature": [ " ", @@ -2009,7 +2009,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 110 + "lineNumber": 112 }, "signature": [ "(request: ", @@ -2038,7 +2038,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 111 + "lineNumber": 113 }, "signature": [ "{ asScoped: (request: ", @@ -2063,7 +2063,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 97 + "lineNumber": 99 }, "initialIsOpen": false }, @@ -2094,7 +2094,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 73 + "lineNumber": 75 }, "signature": [ "(request: SearchStrategyRequest, options: ", @@ -2126,7 +2126,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 78 + "lineNumber": 80 }, "signature": [ "((id: string, options: ", @@ -2156,7 +2156,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 79 + "lineNumber": 81 }, "signature": [ "((id: string, keepAlive: string, options: ", @@ -2181,7 +2181,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 69 + "lineNumber": 71 }, "initialIsOpen": false }, @@ -2200,7 +2200,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 34 + "lineNumber": 36 }, "signature": [ "Pick<", @@ -2211,7 +2211,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { @@ -2222,7 +2222,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 35 + "lineNumber": 37 }, "signature": [ { @@ -2242,7 +2242,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 36 + "lineNumber": 38 }, "signature": [ { @@ -2262,7 +2262,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 37 + "lineNumber": 39 }, "signature": [ { @@ -2278,7 +2278,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 33 + "lineNumber": 35 }, "initialIsOpen": false }, @@ -2344,7 +2344,23 @@ } ], "enums": [], - "misc": [], + "misc": [ + { + "id": "def-server.SearchRequestHandlerContext", + "type": "Type", + "label": "SearchRequestHandlerContext", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 118 + }, + "signature": [ + "IScopedSearchClient" + ], + "initialIsOpen": false + } + ], "objects": [] }, "common": { @@ -9455,6 +9471,70 @@ "returnComment": [], "initialIsOpen": false }, + { + "id": "def-common.getKibanaContextFn", + "type": "Function", + "children": [ + { + "type": "Function", + "label": "getStartDependencies", + "isRequired": true, + "signature": [ + "(getKibanaRequest: (() => ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") | undefined) => Promise<", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.KibanaContextStartDependencies", + "text": "KibanaContextStartDependencies" + }, + ">" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context.ts", + "lineNumber": 52 + } + } + ], + "signature": [ + "(getStartDependencies: (getKibanaRequest: (() => ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") | undefined) => Promise<", + "KibanaContextStartDependencies", + ">) => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.ExpressionFunctionKibanaContext", + "text": "ExpressionFunctionKibanaContext" + } + ], + "description": [], + "label": "getKibanaContextFn", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context.ts", + "lineNumber": 51 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + }, { "id": "def-common.getMaxMetricAgg", "type": "Function", @@ -10015,8 +10095,7 @@ "docId": "kibDataSearchPluginApi", "section": "def-common.ISearchRequestParams", "text": "ISearchRequestParams" - }, - ">" + } ], "description": [], "children": [ @@ -15167,7 +15246,7 @@ ], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 33 + "lineNumber": 36 }, "signature": [ { @@ -15477,7 +15556,7 @@ "section": "def-common.ISearchRequestParams", "text": "ISearchRequestParams" }, - ">>" + ">" ], "description": [], "tags": [], @@ -15490,7 +15569,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 20 + "lineNumber": 19 }, "signature": [ "string | undefined" @@ -15499,7 +15578,7 @@ ], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 19 + "lineNumber": 18 }, "initialIsOpen": false }, @@ -16090,7 +16169,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 36 + "lineNumber": 35 }, "signature": [ "(params: { body: ", @@ -16120,7 +16199,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 40 + "lineNumber": 39 }, "signature": [ "BehaviorSubject", @@ -16130,7 +16209,7 @@ ], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 35 + "lineNumber": 34 }, "initialIsOpen": false }, @@ -16224,7 +16303,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 26 + "lineNumber": 25 }, "signature": [ "MsearchRequest[]" @@ -16233,7 +16312,7 @@ ], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 25 + "lineNumber": 24 }, "initialIsOpen": false }, @@ -16252,21 +16331,19 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 31 + "lineNumber": 30 }, "signature": [ "ApiResponse", "<{ responses: ", "SearchResponse", - "[]; }, ", - "Context", - ">" + "[]; }, unknown>" ] } ], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 30 + "lineNumber": 29 }, "initialIsOpen": false }, @@ -16613,7 +16690,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 37 + "lineNumber": 40 } }, { @@ -16624,7 +16701,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 38 + "lineNumber": 41 } }, { @@ -16635,7 +16712,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 39 + "lineNumber": 42 } }, { @@ -16646,7 +16723,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 40 + "lineNumber": 43 } }, { @@ -16657,7 +16734,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 41 + "lineNumber": 44 } }, { @@ -16668,13 +16745,13 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 42 + "lineNumber": 45 } } ], "source": { "path": "src/plugins/data/common/search/search_source/fetch/types.ts", - "lineNumber": 36 + "lineNumber": 39 }, "initialIsOpen": false }, @@ -17568,7 +17645,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 49 + "lineNumber": 48 } }, { @@ -17579,7 +17656,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 50 + "lineNumber": 49 }, "signature": [ "(params: ", @@ -17604,7 +17681,7 @@ ], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 48 + "lineNumber": 47 }, "initialIsOpen": false }, @@ -17633,7 +17710,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 54 + "lineNumber": 53 }, "signature": [ "Promise<", @@ -17649,7 +17726,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 55 + "lineNumber": 54 }, "signature": [ "() => void" @@ -17658,7 +17735,7 @@ ], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 53 + "lineNumber": 52 }, "initialIsOpen": false }, @@ -17694,7 +17771,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 44 + "lineNumber": 43 }, "signature": [ "Record[]" @@ -17703,7 +17780,7 @@ ], "source": { "path": "src/plugins/data/common/search/search_source/legacy/types.ts", - "lineNumber": 43 + "lineNumber": 42 }, "initialIsOpen": false }, @@ -18653,7 +18730,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 13 + "lineNumber": 12 }, "signature": [ "\"es\"" @@ -18877,7 +18954,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 26 + "lineNumber": 34 }, "signature": [ "ExpressionFunctionDefinition<\"kibana_context\", ", @@ -19315,10 +19392,10 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 23 + "lineNumber": 22 }, "signature": [ - "IKibanaSearchResponse>" + "IKibanaSearchResponse>" ], "initialIsOpen": false }, @@ -19530,10 +19607,10 @@ "description": [], "source": { "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 15 + "lineNumber": 14 }, "signature": [ - "{ trackTotalHits?: boolean | undefined; } & Search" + "{ trackTotalHits?: boolean | undefined; } & estypes.SearchRequest" ], "initialIsOpen": false }, @@ -20649,426 +20726,6 @@ }, "initialIsOpen": false }, - { - "id": "def-common.kibanaContextFunction", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.kibanaContextFunction.name", - "type": "string", - "label": "name", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 44 - }, - "signature": [ - "\"kibana_context\"" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.type", - "type": "string", - "label": "type", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 45 - }, - "signature": [ - "\"kibana_context\"" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.inputTypes", - "type": "Array", - "label": "inputTypes", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 46 - }, - "signature": [ - "(\"kibana_context\" | \"null\")[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.help", - "type": "string", - "label": "help", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 47 - } - }, - { - "id": "def-common.kibanaContextFunction.args", - "type": "Object", - "tags": [], - "children": [ - { - "id": "def-common.kibanaContextFunction.args.q", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.q.types", - "type": "Array", - "label": "types", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 52 - }, - "signature": [ - "(\"null\" | \"kibana_query\")[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.q.aliases", - "type": "Array", - "label": "aliases", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 53 - }, - "signature": [ - "string[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.q.default", - "type": "Uncategorized", - "label": "default", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 54 - }, - "signature": [ - "null" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.q.help", - "type": "string", - "label": "help", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 55 - } - } - ], - "description": [], - "label": "q", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 51 - } - }, - { - "id": "def-common.kibanaContextFunction.args.filters", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.filters.types", - "type": "Array", - "label": "types", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 60 - }, - "signature": [ - "(\"null\" | \"kibana_filter\")[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.filters.multi", - "type": "boolean", - "label": "multi", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 61 - }, - "signature": [ - "true" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.filters.help", - "type": "string", - "label": "help", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 62 - } - } - ], - "description": [], - "label": "filters", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 59 - } - }, - { - "id": "def-common.kibanaContextFunction.args.timeRange", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.timeRange.types", - "type": "Array", - "label": "types", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 67 - }, - "signature": [ - "(\"null\" | \"timerange\")[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.timeRange.default", - "type": "Uncategorized", - "label": "default", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 68 - }, - "signature": [ - "null" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.timeRange.help", - "type": "string", - "label": "help", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 69 - } - } - ], - "description": [], - "label": "timeRange", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 66 - } - }, - { - "id": "def-common.kibanaContextFunction.args.savedSearchId", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.savedSearchId.types", - "type": "Array", - "label": "types", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 74 - }, - "signature": [ - "(\"string\" | \"null\")[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.savedSearchId.default", - "type": "Uncategorized", - "label": "default", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 75 - }, - "signature": [ - "null" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.savedSearchId.help", - "type": "string", - "label": "help", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 76 - } - } - ], - "description": [], - "label": "savedSearchId", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 73 - } - } - ], - "description": [], - "label": "args", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 50 - } - }, - { - "id": "def-common.kibanaContextFunction.fn", - "type": "Function", - "label": "fn", - "signature": [ - "(input: Input, args: Arguments, { getSavedObject }: ", - { - "pluginId": "expressions", - "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExecutionContext", - "text": "ExecutionContext" - }, - "<", - { - "pluginId": "inspector", - "scope": "common", - "docId": "kibInspectorPluginApi", - "section": "def-common.Adapters", - "text": "Adapters" - }, - ", ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.ExecutionContextSearch", - "text": "ExecutionContextSearch" - }, - ">) => Promise<{ type: \"kibana_context\"; query: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataQueryPluginApi", - "section": "def-common.Query", - "text": "Query" - }, - "[]; filters: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.Filter", - "text": "Filter" - } - ], - "description": [], - "children": [ - { - "type": "CompoundType", - "label": "input", - "isRequired": false, - "signature": [ - "Input" - ], - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 82 - } - }, - { - "type": "Object", - "label": "args", - "isRequired": true, - "signature": [ - "Arguments" - ], - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 82 - } - }, - { - "type": "Object", - "label": "{ getSavedObject }", - "isRequired": true, - "signature": [ - { - "pluginId": "expressions", - "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExecutionContext", - "text": "ExecutionContext" - }, - "<", - { - "pluginId": "inspector", - "scope": "common", - "docId": "kibInspectorPluginApi", - "section": "def-common.Adapters", - "text": "Adapters" - }, - ", ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.ExecutionContextSearch", - "text": "ExecutionContextSearch" - }, - ">" - ], - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 82 - } - } - ], - "tags": [], - "returnComment": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 82 - } - } - ], - "description": [], - "label": "kibanaContextFunction", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 43 - }, - "initialIsOpen": false - }, { "id": "def-common.kibanaFilterFunction", "type": "Object", diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 70cbd23125248..370bd2ffd101e 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -36,6 +36,9 @@ import dataSearchObj from './data_search.json'; ### Interfaces +### Consts, variables and types + + ## Common ### Objects diff --git a/api_docs/event_log.json b/api_docs/event_log.json index 64266b0563556..aea4d0c001913 100644 --- a/api_docs/event_log.json +++ b/api_docs/event_log.json @@ -225,7 +225,7 @@ "description": [], "source": { "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", - "lineNumber": 35 + "lineNumber": 36 } }, { @@ -236,7 +236,7 @@ "description": [], "source": { "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", - "lineNumber": 36 + "lineNumber": 37 } }, { @@ -247,7 +247,7 @@ "description": [], "source": { "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", - "lineNumber": 37 + "lineNumber": 38 } }, { @@ -258,7 +258,7 @@ "description": [], "source": { "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", - "lineNumber": 38 + "lineNumber": 39 }, "signature": [ "(Readonly<{ '@timestamp'?: string | undefined; kibana?: Readonly<{ server_uuid?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; } & {}>[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ message?: string | undefined; } & {}> | undefined; message?: string | undefined; tags?: string[] | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; end?: string | undefined; action?: string | undefined; provider?: string | undefined; duration?: number | undefined; outcome?: string | undefined; reason?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" @@ -267,7 +267,7 @@ ], "source": { "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", - "lineNumber": 34 + "lineNumber": 35 }, "initialIsOpen": false } diff --git a/api_docs/expressions.json b/api_docs/expressions.json index eefffb009be2a..06c97e497ae41 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -1562,7 +1562,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 223 + "lineNumber": 230 } } ], @@ -1570,7 +1570,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 223 + "lineNumber": 230 } }, { @@ -1606,7 +1606,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } }, { @@ -1619,7 +1619,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } } ], @@ -1627,7 +1627,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } }, { @@ -1670,7 +1670,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } }, { @@ -1683,7 +1683,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } } ], @@ -1691,7 +1691,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } }, { @@ -1715,7 +1715,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 250 + "lineNumber": 258 } } ], @@ -2975,7 +2975,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 40 + "lineNumber": 35 } } ], @@ -2983,7 +2983,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 40 + "lineNumber": 35 } }, { @@ -3028,7 +3028,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 56 + "lineNumber": 45 } } ], @@ -3036,7 +3036,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 56 + "lineNumber": 45 } }, { @@ -3079,7 +3079,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 70 + "lineNumber": 59 } } ], @@ -3087,7 +3087,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 70 + "lineNumber": 59 } }, { @@ -3103,13 +3103,13 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 86 + "lineNumber": 75 } } ], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 37 + "lineNumber": 32 }, "initialIsOpen": false }, @@ -3180,7 +3180,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 40 + "lineNumber": 35 } } ], @@ -3188,7 +3188,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 40 + "lineNumber": 35 } }, { @@ -3233,7 +3233,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 56 + "lineNumber": 45 } } ], @@ -3241,7 +3241,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 56 + "lineNumber": 45 } }, { @@ -3284,7 +3284,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 70 + "lineNumber": 59 } } ], @@ -3292,7 +3292,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 70 + "lineNumber": 59 } }, { @@ -3308,13 +3308,13 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 86 + "lineNumber": 75 } } ], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 37 + "lineNumber": 32 }, "initialIsOpen": false }, @@ -5908,7 +5908,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 28 + "lineNumber": 27 }, "signature": [ "() => ExecutionContextSearch" @@ -5924,7 +5924,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 33 + "lineNumber": 32 }, "signature": [ "Record" @@ -5940,7 +5940,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 38 + "lineNumber": 37 }, "signature": [ "Record string | undefined" @@ -6012,7 +6012,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 60 + "lineNumber": 59 }, "signature": [ "(() => ", @@ -6026,46 +6026,6 @@ ") | undefined" ] }, - { - "tags": [], - "id": "def-public.ExecutionContext.getSavedObject", - "type": "Function", - "label": "getSavedObject", - "description": [ - "\nAllows to fetch saved objects from ElasticSearch. In browser `getSavedObject`\nfunction is provided automatically by the Expressions plugin. On the server\nthe caller of the expression has to provide this context function. The\nreason is because on the browser we always know the user who tries to\nfetch a saved object, thus saved object client is scoped automatically to\nthat user. However, on the server we can scope that saved object client to\nany user, or even not scope it at all and execute it as an \"internal\" user." - ], - "source": { - "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 71 - }, - "signature": [ - "((type: string, id: string) => Promise<", - { - "pluginId": "core", - "scope": "common", - "docId": "kibCorePluginApi", - "section": "def-common.SavedObject", - "text": "SavedObject" - }, - ">) | undefined" - ] - }, { "tags": [], "id": "def-public.ExecutionContext.isSyncColorsEnabled", @@ -6076,7 +6036,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 79 + "lineNumber": 64 }, "signature": [ "(() => boolean) | undefined" @@ -6085,7 +6045,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 21 + "lineNumber": 20 }, "initialIsOpen": false }, @@ -9481,7 +9441,7 @@ ], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 24 + "lineNumber": 19 }, "signature": [ "{ readonly getType: (name: string) => ", @@ -9562,7 +9522,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 30 + "lineNumber": 25 }, "signature": [ "typeof ", @@ -9583,7 +9543,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 31 + "lineNumber": 26 }, "signature": [ "typeof ", @@ -9604,7 +9564,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 32 + "lineNumber": 27 }, "signature": [ { @@ -9624,7 +9584,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 33 + "lineNumber": 28 }, "signature": [ "({ className, dataAttrs, padding, renderError, expression, onEvent, onData$, reload$, debounce, ...expressionLoaderOptions }: ", @@ -9646,7 +9606,7 @@ "description": [], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 34 + "lineNumber": 29 }, "signature": [ "typeof ", @@ -9662,7 +9622,7 @@ ], "source": { "path": "src/plugins/expressions/public/plugin.ts", - "lineNumber": 29 + "lineNumber": 24 }, "lifecycle": "start", "initialIsOpen": true @@ -11035,7 +10995,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 223 + "lineNumber": 230 } } ], @@ -11043,7 +11003,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 223 + "lineNumber": 230 } }, { @@ -11079,7 +11039,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } }, { @@ -11092,7 +11052,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } } ], @@ -11100,7 +11060,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } }, { @@ -11143,7 +11103,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } }, { @@ -11156,7 +11116,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } } ], @@ -11164,7 +11124,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } }, { @@ -11188,7 +11148,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 250 + "lineNumber": 258 } } ], @@ -13984,7 +13944,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 28 + "lineNumber": 27 }, "signature": [ "() => ExecutionContextSearch" @@ -14000,7 +13960,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 33 + "lineNumber": 32 }, "signature": [ "Record" @@ -14016,7 +13976,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 38 + "lineNumber": 37 }, "signature": [ "Record string | undefined" @@ -14088,7 +14048,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 60 + "lineNumber": 59 }, "signature": [ "(() => ", @@ -14102,46 +14062,6 @@ ") | undefined" ] }, - { - "tags": [], - "id": "def-server.ExecutionContext.getSavedObject", - "type": "Function", - "label": "getSavedObject", - "description": [ - "\nAllows to fetch saved objects from ElasticSearch. In browser `getSavedObject`\nfunction is provided automatically by the Expressions plugin. On the server\nthe caller of the expression has to provide this context function. The\nreason is because on the browser we always know the user who tries to\nfetch a saved object, thus saved object client is scoped automatically to\nthat user. However, on the server we can scope that saved object client to\nany user, or even not scope it at all and execute it as an \"internal\" user." - ], - "source": { - "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 71 - }, - "signature": [ - "((type: string, id: string) => Promise<", - { - "pluginId": "core", - "scope": "common", - "docId": "kibCorePluginApi", - "section": "def-common.SavedObject", - "text": "SavedObject" - }, - ">) | undefined" - ] - }, { "tags": [], "id": "def-server.ExecutionContext.isSyncColorsEnabled", @@ -14152,7 +14072,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 79 + "lineNumber": 64 }, "signature": [ "(() => boolean) | undefined" @@ -14161,7 +14081,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 21 + "lineNumber": 20 }, "initialIsOpen": false }, @@ -18395,7 +18315,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 223 + "lineNumber": 230 } } ], @@ -18403,7 +18323,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 223 + "lineNumber": 230 } }, { @@ -18439,7 +18359,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } }, { @@ -18452,7 +18372,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } } ], @@ -18460,7 +18380,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 233 + "lineNumber": 241 } }, { @@ -18503,7 +18423,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } }, { @@ -18516,7 +18436,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } } ], @@ -18524,7 +18444,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 241 + "lineNumber": 249 } }, { @@ -18548,7 +18468,7 @@ "returnComment": [], "source": { "path": "src/plugins/expressions/common/executor/executor.ts", - "lineNumber": 250 + "lineNumber": 258 } } ], @@ -23230,7 +23150,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 86 + "lineNumber": 71 }, "signature": [ { @@ -23250,7 +23170,7 @@ "description": [], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 87 + "lineNumber": 72 }, "signature": [ { @@ -23265,7 +23185,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 85 + "lineNumber": 70 }, "initialIsOpen": false }, @@ -23362,7 +23282,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 28 + "lineNumber": 27 }, "signature": [ "() => ExecutionContextSearch" @@ -23378,7 +23298,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 33 + "lineNumber": 32 }, "signature": [ "Record" @@ -23394,7 +23314,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 38 + "lineNumber": 37 }, "signature": [ "Record string | undefined" @@ -23466,7 +23386,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 60 + "lineNumber": 59 }, "signature": [ "(() => ", @@ -23480,46 +23400,6 @@ ") | undefined" ] }, - { - "tags": [], - "id": "def-common.ExecutionContext.getSavedObject", - "type": "Function", - "label": "getSavedObject", - "description": [ - "\nAllows to fetch saved objects from ElasticSearch. In browser `getSavedObject`\nfunction is provided automatically by the Expressions plugin. On the server\nthe caller of the expression has to provide this context function. The\nreason is because on the browser we always know the user who tries to\nfetch a saved object, thus saved object client is scoped automatically to\nthat user. However, on the server we can scope that saved object client to\nany user, or even not scope it at all and execute it as an \"internal\" user." - ], - "source": { - "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 71 - }, - "signature": [ - "((type: string, id: string) => Promise<", - { - "pluginId": "core", - "scope": "common", - "docId": "kibCorePluginApi", - "section": "def-common.SavedObject", - "text": "SavedObject" - }, - ">) | undefined" - ] - }, { "tags": [], "id": "def-common.ExecutionContext.isSyncColorsEnabled", @@ -23530,7 +23410,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 79 + "lineNumber": 64 }, "signature": [ "(() => boolean) | undefined" @@ -23539,7 +23419,7 @@ ], "source": { "path": "src/plugins/expressions/common/execution/types.ts", - "lineNumber": 21 + "lineNumber": 20 }, "initialIsOpen": false }, diff --git a/api_docs/fleet.json b/api_docs/fleet.json index ed51f88ee9d5d..60d0dca4d8a10 100644 --- a/api_docs/fleet.json +++ b/api_docs/fleet.json @@ -1789,7 +1789,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, id: string, withPackagePolicies?: boolean) => Promise<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, id: string, withPackagePolicies?: boolean) => Promise<", { "pluginId": "fleet", "scope": "common", @@ -1819,7 +1819,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, options: Readonly<{ page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: \"asc\" | \"desc\" | undefined; kuery?: any; showUpgradeable?: boolean | undefined; } & {}> & { withPackagePolicies?: boolean | undefined; }) => Promise<{ items: ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, options: Readonly<{ page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: \"asc\" | \"desc\" | undefined; kuery?: any; showUpgradeable?: boolean | undefined; } & {}> & { withPackagePolicies?: boolean | undefined; }) => Promise<{ items: ", { "pluginId": "fleet", "scope": "common", @@ -1849,7 +1849,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">) => Promise" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">) => Promise" ] }, { @@ -1871,7 +1871,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, id: string, options?: { standalone: boolean; } | undefined) => Promise<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, id: string, options?: { standalone: boolean; } | undefined) => Promise<", { "pluginId": "fleet", "scope": "common", @@ -2434,7 +2434,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, pkgName: string, datasetPath: string) => Promise" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, pkgName: string, datasetPath: string) => Promise" ], "description": [], "children": [ @@ -2451,7 +2451,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "description": [], "source": { @@ -2661,7 +2661,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, pkgName: string) => Promise<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, pkgName: string) => Promise<", { "pluginId": "fleet", "scope": "common", @@ -2686,7 +2686,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "description": [], "source": { @@ -3744,7 +3744,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 152 + "lineNumber": 151 } }, { @@ -3755,7 +3755,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 153 + "lineNumber": 152 }, "signature": [ { @@ -3776,7 +3776,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 154 + "lineNumber": 153 }, "signature": [ "string | undefined" @@ -3790,7 +3790,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 155 + "lineNumber": 154 }, "signature": [ "string | undefined" @@ -3804,7 +3804,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 156 + "lineNumber": 155 }, "signature": [ "string[]" @@ -3813,7 +3813,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 151 + "lineNumber": 150 }, "initialIsOpen": false }, @@ -3849,7 +3849,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 49 + "lineNumber": 48 }, "signature": [ { @@ -3869,7 +3869,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 50 + "lineNumber": 49 }, "signature": [ "any" @@ -3883,7 +3883,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 51 + "lineNumber": 50 }, "signature": [ "string | undefined" @@ -3897,7 +3897,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 52 + "lineNumber": 51 } }, { @@ -3908,7 +3908,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 53 + "lineNumber": 52 } }, { @@ -3919,7 +3919,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 54 + "lineNumber": 53 } }, { @@ -3930,7 +3930,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 55 + "lineNumber": 54 }, "signature": [ "any" @@ -3939,7 +3939,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 48 + "lineNumber": 47 }, "initialIsOpen": false }, @@ -3975,13 +3975,13 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 124 + "lineNumber": 123 } } ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 123 + "lineNumber": 122 }, "initialIsOpen": false }, @@ -4000,7 +4000,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 130 + "lineNumber": 129 }, "signature": [ "any" @@ -4009,7 +4009,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 129 + "lineNumber": 128 }, "initialIsOpen": false }, @@ -4174,7 +4174,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 59 + "lineNumber": 58 } }, { @@ -4185,7 +4185,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 60 + "lineNumber": 59 }, "signature": [ { @@ -4205,7 +4205,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 61 + "lineNumber": 60 }, "signature": [ "{ policy: ", @@ -4227,7 +4227,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 64 + "lineNumber": 63 } }, { @@ -4238,7 +4238,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 65 + "lineNumber": 64 } }, { @@ -4249,7 +4249,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 66 + "lineNumber": 65 } }, { @@ -4260,7 +4260,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 67 + "lineNumber": 66 }, "signature": [ "any" @@ -4269,7 +4269,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 58 + "lineNumber": 57 }, "initialIsOpen": false }, @@ -4298,7 +4298,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 160 + "lineNumber": 159 }, "signature": [ "string | undefined" @@ -4312,7 +4312,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 161 + "lineNumber": 160 }, "signature": [ "string[] | undefined" @@ -4321,7 +4321,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 159 + "lineNumber": 158 }, "initialIsOpen": false }, @@ -5111,7 +5111,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 193 + "lineNumber": 200 }, "signature": [ "{ agentId: string; }" @@ -5120,7 +5120,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 192 + "lineNumber": 199 }, "initialIsOpen": false }, @@ -5521,7 +5521,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 173 + "lineNumber": 172 }, "signature": [ "number | undefined" @@ -5537,7 +5537,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 177 + "lineNumber": 176 }, "signature": [ "string | undefined" @@ -5553,7 +5553,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 181 + "lineNumber": 180 }, "signature": [ { @@ -5575,7 +5575,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 185 + "lineNumber": 184 } }, { @@ -5588,7 +5588,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 189 + "lineNumber": 188 } }, { @@ -5601,7 +5601,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 193 + "lineNumber": 192 }, "signature": [ "string | undefined" @@ -5617,7 +5617,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 197 + "lineNumber": 196 }, "signature": [ "string | undefined" @@ -5633,7 +5633,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 201 + "lineNumber": 200 }, "signature": [ "string | null | undefined" @@ -5649,7 +5649,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 205 + "lineNumber": 204 }, "signature": [ "string | null | undefined" @@ -5665,7 +5665,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 209 + "lineNumber": 208 }, "signature": [ "string | undefined" @@ -5679,7 +5679,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 210 + "lineNumber": 209 }, "signature": [ { @@ -5702,7 +5702,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 214 + "lineNumber": 213 }, "signature": [ { @@ -5724,7 +5724,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 218 + "lineNumber": 217 }, "signature": [ { @@ -5746,7 +5746,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 222 + "lineNumber": 221 }, "signature": [ "string | undefined" @@ -5762,7 +5762,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 226 + "lineNumber": 225 }, "signature": [ "number | null | undefined" @@ -5778,7 +5778,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 230 + "lineNumber": 229 }, "signature": [ "number | undefined" @@ -5794,7 +5794,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 234 + "lineNumber": 233 }, "signature": [ "string | undefined" @@ -5810,7 +5810,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 238 + "lineNumber": 237 }, "signature": [ "string | undefined" @@ -5826,7 +5826,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 242 + "lineNumber": 241 }, "signature": [ "\"online\" | \"error\" | \"updating\" | \"degraded\" | undefined" @@ -5842,7 +5842,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 246 + "lineNumber": 245 }, "signature": [ "string | undefined" @@ -5858,7 +5858,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 250 + "lineNumber": 249 }, "signature": [ "string | undefined" @@ -5874,7 +5874,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 254 + "lineNumber": 253 }, "signature": [ "string | undefined" @@ -5890,7 +5890,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 258 + "lineNumber": 257 }, "signature": [ "string[] | undefined" @@ -5906,7 +5906,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 262 + "lineNumber": 261 }, "signature": [ "number | undefined" @@ -5915,7 +5915,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 169 + "lineNumber": 168 }, "initialIsOpen": false }, @@ -5938,7 +5938,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 286 + "lineNumber": 285 }, "signature": [ "string | undefined" @@ -5954,7 +5954,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 290 + "lineNumber": 289 }, "signature": [ "number | undefined" @@ -5970,7 +5970,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 294 + "lineNumber": 293 }, "signature": [ "string | undefined" @@ -5986,7 +5986,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 298 + "lineNumber": 297 }, "signature": [ "string | undefined" @@ -6002,7 +6002,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 302 + "lineNumber": 301 }, "signature": [ "string | undefined" @@ -6018,7 +6018,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 306 + "lineNumber": 305 }, "signature": [ "string | undefined" @@ -6034,7 +6034,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 310 + "lineNumber": 309 }, "signature": [ "string | undefined" @@ -6050,7 +6050,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 314 + "lineNumber": 313 }, "signature": [ "string[] | undefined" @@ -6066,7 +6066,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 318 + "lineNumber": 317 }, "signature": [ "{ [k: string]: unknown; } | undefined" @@ -6080,7 +6080,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 321 + "lineNumber": 320 }, "signature": [ "any" @@ -6089,7 +6089,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 282 + "lineNumber": 281 }, "initialIsOpen": false }, @@ -6112,7 +6112,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 271 + "lineNumber": 270 } }, { @@ -6125,7 +6125,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 275 + "lineNumber": 274 } }, { @@ -6136,7 +6136,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 276 + "lineNumber": 275 }, "signature": [ "any" @@ -6145,7 +6145,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 267 + "lineNumber": 266 }, "initialIsOpen": false }, @@ -6968,7 +6968,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 208 + "lineNumber": 215 }, "signature": [ "{ kuery?: string | undefined; policyId?: string | undefined; }" @@ -6977,7 +6977,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 207 + "lineNumber": 214 }, "initialIsOpen": false }, @@ -6996,7 +6996,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 215 + "lineNumber": 222 }, "signature": [ "{ events: number; total: number; online: number; error: number; offline: number; other: number; updating: number; }" @@ -7005,7 +7005,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 214 + "lineNumber": 221 }, "initialIsOpen": false }, @@ -7436,7 +7436,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 175 + "lineNumber": 182 }, "signature": [ "{ agentId: string; }" @@ -7450,7 +7450,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 178 + "lineNumber": 185 }, "signature": [ "{ page: number; perPage: number; kuery?: string | undefined; }" @@ -7459,7 +7459,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 174 + "lineNumber": 181 }, "initialIsOpen": false }, @@ -7478,7 +7478,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 186 + "lineNumber": 193 }, "signature": [ { @@ -7499,7 +7499,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 187 + "lineNumber": 194 } }, { @@ -7510,7 +7510,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 188 + "lineNumber": 195 } }, { @@ -7521,13 +7521,13 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 189 + "lineNumber": 196 } } ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 185 + "lineNumber": 192 }, "initialIsOpen": false }, @@ -8883,7 +8883,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 43 + "lineNumber": 42 }, "signature": [ { @@ -8903,7 +8903,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 44 + "lineNumber": 43 }, "signature": [ "any" @@ -8917,7 +8917,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 45 + "lineNumber": 44 }, "signature": [ "string | undefined" @@ -8926,7 +8926,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 42 + "lineNumber": 41 }, "initialIsOpen": false }, @@ -8945,7 +8945,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 98 + "lineNumber": 97 }, "signature": [ "\"STATE\" | \"ERROR\" | \"ACTION_RESULT\" | \"ACTION\"" @@ -8959,7 +8959,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 99 + "lineNumber": 98 }, "signature": [ "\"RUNNING\" | \"STARTING\" | \"IN_PROGRESS\" | \"CONFIG\" | \"FAILED\" | \"STOPPING\" | \"STOPPED\" | \"DEGRADED\" | \"UPDATING\" | \"DATA_DUMP\" | \"ACKNOWLEDGED\" | \"UNKNOWN\"" @@ -8973,7 +8973,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 114 + "lineNumber": 113 } }, { @@ -8984,7 +8984,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 115 + "lineNumber": 114 } }, { @@ -8995,7 +8995,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 116 + "lineNumber": 115 }, "signature": [ "any" @@ -9009,7 +9009,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 117 + "lineNumber": 116 } }, { @@ -9020,7 +9020,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 118 + "lineNumber": 117 }, "signature": [ "string | undefined" @@ -9034,7 +9034,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 119 + "lineNumber": 118 }, "signature": [ "string | undefined" @@ -9048,7 +9048,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 120 + "lineNumber": 119 }, "signature": [ "string | undefined" @@ -9057,7 +9057,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 97 + "lineNumber": 96 }, "initialIsOpen": false }, @@ -10708,7 +10708,7 @@ "children": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 148 + "lineNumber": 154 }, "initialIsOpen": false }, @@ -10727,7 +10727,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 161 + "lineNumber": 167 }, "signature": [ "{ policy_id: string; agents: string | string[]; }" @@ -10736,35 +10736,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 160 - }, - "initialIsOpen": false - }, - { - "id": "def-common.PostBulkAgentReassignResponse", - "type": "Interface", - "label": "PostBulkAgentReassignResponse", - "description": [], - "tags": [], - "children": [ - { - "id": "def-common.PostBulkAgentReassignResponse.Unnamed", - "type": "Any", - "label": "Unnamed", - "tags": [], - "description": [], - "source": { - "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 168 - }, - "signature": [ - "any" - ] - } - ], - "source": { - "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 167 + "lineNumber": 166 }, "initialIsOpen": false }, @@ -10837,19 +10809,6 @@ }, "initialIsOpen": false }, - { - "id": "def-common.PostBulkAgentUpgradeResponse", - "type": "Interface", - "label": "PostBulkAgentUpgradeResponse", - "description": [], - "tags": [], - "children": [], - "source": { - "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 145 - }, - "initialIsOpen": false - }, { "id": "def-common.PostEnrollmentAPIKeyRequest", "type": "Interface", @@ -11047,7 +11006,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 151 + "lineNumber": 157 }, "signature": [ "{ agentId: string; }" @@ -11061,7 +11020,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 154 + "lineNumber": 160 }, "signature": [ "{ policy_id: string; }" @@ -11070,7 +11029,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 150 + "lineNumber": 156 }, "initialIsOpen": false }, @@ -11083,7 +11042,7 @@ "children": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 158 + "lineNumber": 164 }, "initialIsOpen": false }, @@ -12138,7 +12097,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 199 + "lineNumber": 206 }, "signature": [ "{ agentId: string; }" @@ -12152,7 +12111,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 202 + "lineNumber": 209 }, "signature": [ "{ user_provided_metadata: Record; }" @@ -12161,7 +12120,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "lineNumber": 198 + "lineNumber": 205 }, "initialIsOpen": false }, @@ -12597,7 +12556,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 88 + "lineNumber": 87 }, "signature": [ "CommonAgentActionSOAttributes & { agent_id: string; }" @@ -12615,7 +12574,7 @@ "lineNumber": 34 }, "signature": [ - "\"POLICY_CHANGE\" | \"UNENROLL\" | \"UPGRADE\" | \"SETTINGS\" | \"INTERNAL_POLICY_REASSIGN\"" + "\"POLICY_CHANGE\" | \"UNENROLL\" | \"UPGRADE\" | \"SETTINGS\" | \"POLICY_REASSIGN\"" ], "initialIsOpen": false }, @@ -12642,7 +12601,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 127 + "lineNumber": 126 }, "signature": [ "NewAgentEvent" @@ -12657,7 +12616,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 91 + "lineNumber": 90 }, "signature": [ "CommonAgentActionSOAttributes & { policy_id: string; policy_revision: number; }" @@ -12672,7 +12631,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 72 + "lineNumber": 71 }, "signature": [ "Pick & { type: 'CONFIG_CHANGE'; data: { config: FullAgentPolicy;}; }" @@ -12920,7 +12879,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "lineNumber": 95 + "lineNumber": 94 }, "signature": [ { @@ -13822,6 +13781,36 @@ ], "initialIsOpen": false }, + { + "id": "def-common.PostBulkAgentReassignResponse", + "type": "Type", + "label": "PostBulkAgentReassignResponse", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", + "lineNumber": 173 + }, + "signature": [ + "{ [x: string]: { success: boolean; error?: string | undefined; }; }" + ], + "initialIsOpen": false + }, + { + "id": "def-common.PostBulkAgentUpgradeResponse", + "type": "Type", + "label": "PostBulkAgentUpgradeResponse", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", + "lineNumber": 145 + }, + "signature": [ + "{ [x: string]: { success: boolean; error?: string | undefined; }; }" + ], + "initialIsOpen": false + }, { "id": "def-common.RegistryPackage", "type": "Type", diff --git a/api_docs/global_search.json b/api_docs/global_search.json index 985abf3417935..0cb40be77a7ae 100644 --- a/api_docs/global_search.json +++ b/api_docs/global_search.json @@ -704,7 +704,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">; typeRegistry: Pick<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">; typeRegistry: Pick<", { "pluginId": "core", "scope": "server", diff --git a/api_docs/index_management.json b/api_docs/index_management.json index 8c6ba711642ff..6c66a94bf59f5 100644 --- a/api_docs/index_management.json +++ b/api_docs/index_management.json @@ -1121,13 +1121,7 @@ "lineNumber": 58 }, "signature": [ - { - "pluginId": "indexManagement", - "scope": "common", - "docId": "kibIndexManagementPluginApi", - "section": "def-common.Health", - "text": "Health" - } + "ClusterStatus" ] }, { diff --git a/api_docs/infra.json b/api_docs/infra.json index a10be8ce8bf04..9d64df881fc52 100644 --- a/api_docs/infra.json +++ b/api_docs/infra.json @@ -198,7 +198,7 @@ "description": [], "source": { "path": "x-pack/plugins/infra/server/plugin.ts", - "lineNumber": 64 + "lineNumber": 65 }, "signature": [ "{ readonly sources?: Readonly<{ default?: Readonly<{ fields?: Readonly<{ host?: string | undefined; message?: string[] | undefined; timestamp?: string | undefined; tiebreaker?: string | undefined; container?: string | undefined; pod?: string | undefined; } & {}> | undefined; logAlias?: string | undefined; metricAlias?: string | undefined; } & {}> | undefined; } & {}> | undefined; readonly enabled: boolean; readonly query: Readonly<{} & { partitionSize: number; partitionFactor: number; }>; }" @@ -222,7 +222,7 @@ "description": [], "source": { "path": "x-pack/plugins/infra/server/plugin.ts", - "lineNumber": 75 + "lineNumber": 76 }, "signature": [ "(sourceId: string, sourceProperties: ", @@ -239,7 +239,7 @@ ], "source": { "path": "x-pack/plugins/infra/server/plugin.ts", - "lineNumber": 74 + "lineNumber": 75 }, "lifecycle": "setup", "initialIsOpen": true diff --git a/api_docs/lens.json b/api_docs/lens.json index e586016c22fc3..b5321bf24ba6b 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -82,7 +82,7 @@ "lineNumber": 43 }, "signature": [ - "\"cardinality\"" + "\"unique_count\"" ] } ], @@ -238,7 +238,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx", - "lineNumber": 73 + "lineNumber": 74 }, "signature": [ "\"filters\"" @@ -252,7 +252,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx", - "lineNumber": 74 + "lineNumber": 75 }, "signature": [ "{ filters: ", @@ -269,7 +269,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx", - "lineNumber": 72 + "lineNumber": 73 }, "initialIsOpen": false }, @@ -291,7 +291,7 @@ "lineNumber": 46 }, "signature": [ - "\"range\" | \"filters\" | \"count\" | \"max\" | \"min\" | \"date_histogram\" | \"sum\" | \"terms\" | \"avg\" | \"median\" | \"cumulative_sum\" | \"derivative\" | \"moving_average\" | \"counter_rate\" | \"cardinality\" | \"percentile\" | \"last_value\" | undefined" + "\"range\" | \"filters\" | \"count\" | \"max\" | \"min\" | \"date_histogram\" | \"sum\" | \"average\" | \"terms\" | \"median\" | \"cumulative_sum\" | \"moving_average\" | \"counter_rate\" | \"differences\" | \"unique_count\" | \"percentile\" | \"last_value\" | undefined" ] }, { @@ -538,6 +538,30 @@ "signature": [ "() => boolean" ] + }, + { + "tags": [], + "id": "def-public.LensPublicStart.getXyVisTypes", + "type": "Function", + "label": "getXyVisTypes", + "description": [ + "\nMethod which returns xy VisualizationTypes array keeping this async as to not impact page load bundle" + ], + "source": { + "path": "x-pack/plugins/lens/public/plugin.ts", + "lineNumber": 108 + }, + "signature": [ + "() => Promise<", + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.VisualizationType", + "text": "VisualizationType" + }, + "[]>" + ] } ], "source": { @@ -929,7 +953,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx", - "lineNumber": 59 + "lineNumber": 57 }, "signature": [ "\"terms\"" @@ -943,7 +967,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx", - "lineNumber": 60 + "lineNumber": 58 }, "signature": [ "{ size: number; orderBy: { type: \"alphabetical\"; } | { type: \"column\"; columnId: string; }; orderDirection: \"asc\" | \"desc\"; otherBucket?: boolean | undefined; missingBucket?: boolean | undefined; format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; }" @@ -952,7 +976,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx", - "lineNumber": 58 + "lineNumber": 56 }, "initialIsOpen": false }, @@ -1114,7 +1138,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 420 + "lineNumber": 423 }, "signature": [ { @@ -1134,7 +1158,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 421 + "lineNumber": 424 }, "signature": [ { @@ -1154,7 +1178,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 422 + "lineNumber": 425 }, "signature": [ "\"hide\" | \"inside\" | \"outside\" | undefined" @@ -1168,7 +1192,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 423 + "lineNumber": 426 }, "signature": [ "\"None\" | \"Zero\" | \"Linear\" | \"Carry\" | \"Lookahead\" | undefined" @@ -1182,7 +1206,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 424 + "lineNumber": 427 }, "signature": [ { @@ -1203,7 +1227,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 425 + "lineNumber": 428 }, "signature": [ "string | undefined" @@ -1217,7 +1241,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 426 + "lineNumber": 429 }, "signature": [ "string | undefined" @@ -1231,7 +1255,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 427 + "lineNumber": 430 }, "signature": [ "string | undefined" @@ -1245,7 +1269,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 428 + "lineNumber": 431 }, "signature": [ { @@ -1266,7 +1290,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 429 + "lineNumber": 432 }, "signature": [ { @@ -1287,7 +1311,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 430 + "lineNumber": 433 }, "signature": [ { @@ -1299,11 +1323,25 @@ }, " | undefined" ] + }, + { + "tags": [], + "id": "def-public.XYState.curveType", + "type": "CompoundType", + "label": "curveType", + "description": [], + "source": { + "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", + "lineNumber": 434 + }, + "signature": [ + "\"LINEAR\" | \"CURVE_MONOTONE_X\" | undefined" + ] } ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 419 + "lineNumber": 422 }, "initialIsOpen": false } @@ -1318,10 +1356,10 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 127 + "lineNumber": 126 }, "signature": [ - "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"avg\"; }" + "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"average\"; }" ], "initialIsOpen": false }, @@ -1380,12 +1418,12 @@ "tags": [], "description": [], "source": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx", - "lineNumber": 35 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx", + "lineNumber": 37 }, "signature": [ "BaseIndexPatternColumn", - " & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & ReferenceBasedIndexPatternColumn & { operationType: 'derivative'; }" + " & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & ReferenceBasedIndexPatternColumn & { operationType: typeof OPERATION_NAME; }" ], "initialIsOpen": false }, @@ -1456,7 +1494,7 @@ "section": "def-public.DateHistogramIndexPatternColumn", "text": "DateHistogramIndexPatternColumn" }, - " | MetricColumn<\"min\"> | MetricColumn<\"max\"> | MetricColumn<\"avg\"> | ", + " | MetricColumn<\"min\"> | MetricColumn<\"max\"> | MetricColumn<\"average\"> | ", { "pluginId": "lens", "scope": "public", @@ -1475,7 +1513,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 129 + "lineNumber": 128 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"max\"; }" @@ -1490,7 +1528,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 130 + "lineNumber": 129 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"median\"; }" @@ -1505,7 +1543,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 128 + "lineNumber": 127 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"min\"; }" @@ -1541,7 +1579,7 @@ "lineNumber": 406 }, "signature": [ - "\"range\" | \"filters\" | \"count\" | \"max\" | \"min\" | \"date_histogram\" | \"sum\" | \"terms\" | \"avg\" | \"median\" | \"cumulative_sum\" | \"derivative\" | \"moving_average\" | \"counter_rate\" | \"cardinality\" | \"percentile\" | \"last_value\"" + "\"range\" | \"filters\" | \"count\" | \"max\" | \"min\" | \"date_histogram\" | \"sum\" | \"average\" | \"terms\" | \"median\" | \"cumulative_sum\" | \"moving_average\" | \"counter_rate\" | \"differences\" | \"unique_count\" | \"percentile\" | \"last_value\"" ], "initialIsOpen": false }, @@ -1598,7 +1636,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 126 + "lineNumber": 125 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"sum\"; }" diff --git a/api_docs/lists.json b/api_docs/lists.json index fe06ebe62ce23..fc935809cd7d8 100644 --- a/api_docs/lists.json +++ b/api_docs/lists.json @@ -3698,6 +3698,188 @@ "lineNumber": 48 }, "initialIsOpen": false + }, + { + "id": "def-server.UpdateExceptionListItemOptions", + "type": "Interface", + "label": "UpdateExceptionListItemOptions", + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions._version", + "type": "string", + "label": "_version", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 144 + }, + "signature": [ + "string | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.comments", + "type": "Array", + "label": "comments", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 145 + }, + "signature": [ + "({ comment: string; } & { id?: string | undefined; })[]" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.entries", + "type": "Array", + "label": "entries", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 146 + }, + "signature": [ + "({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.id", + "type": "string", + "label": "id", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 147 + }, + "signature": [ + "string | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.itemId", + "type": "string", + "label": "itemId", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 148 + }, + "signature": [ + "string | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.namespaceType", + "type": "CompoundType", + "label": "namespaceType", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 149 + }, + "signature": [ + "\"single\" | \"agnostic\"" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.name", + "type": "string", + "label": "name", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 150 + }, + "signature": [ + "string | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.osTypes", + "type": "Array", + "label": "osTypes", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 151 + }, + "signature": [ + "(\"windows\" | \"linux\" | \"macos\")[]" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.description", + "type": "string", + "label": "description", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 152 + }, + "signature": [ + "string | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.meta", + "type": "Uncategorized", + "label": "meta", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 153 + }, + "signature": [ + "object | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.tags", + "type": "Array", + "label": "tags", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 154 + }, + "signature": [ + "string[] | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UpdateExceptionListItemOptions.type", + "type": "string", + "label": "type", + "description": [], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 155 + }, + "signature": [ + "\"simple\" | undefined" + ] + } + ], + "source": { + "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts", + "lineNumber": 143 + }, + "initialIsOpen": false } ], "enums": [], diff --git a/api_docs/ml.json b/api_docs/ml.json index fa9bd613195a6..067ae4c0ea212 100644 --- a/api_docs/ml.json +++ b/api_docs/ml.json @@ -1626,13 +1626,7 @@ "isRequired": false, "signature": [ "Record | undefined" ], "description": [], @@ -1700,210 +1694,6 @@ } ], "interfaces": [ - { - "id": "def-server.AnalysisConfig", - "type": "Interface", - "label": "AnalysisConfig", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.AnalysisConfig.bucket_span", - "type": "string", - "label": "bucket_span", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 48 - } - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.categorization_field_name", - "type": "string", - "label": "categorization_field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 49 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.categorization_filters", - "type": "Array", - "label": "categorization_filters", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 50 - }, - "signature": [ - "string[] | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.categorization_analyzer", - "type": "CompoundType", - "label": "categorization_analyzer", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 51 - }, - "signature": [ - "string | object | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.detectors", - "type": "Array", - "label": "detectors", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 52 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.Detector", - "text": "Detector" - }, - "[]" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.influencers", - "type": "Array", - "label": "influencers", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 53 - }, - "signature": [ - "string[]" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.latency", - "type": "number", - "label": "latency", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 54 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.multivariate_by_fields", - "type": "CompoundType", - "label": "multivariate_by_fields", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 55 - }, - "signature": [ - "boolean | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.summary_count_field_name", - "type": "string", - "label": "summary_count_field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 56 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisConfig.per_partition_categorization", - "type": "Object", - "label": "per_partition_categorization", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 57 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.PerPartitionCategorization", - "text": "PerPartitionCategorization" - }, - " | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 47 - }, - "initialIsOpen": false - }, - { - "id": "def-server.AnalysisLimits", - "type": "Interface", - "label": "AnalysisLimits", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.AnalysisLimits.categorization_examples_limit", - "type": "number", - "label": "categorization_examples_limit", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 73 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.AnalysisLimits.model_memory_limit", - "type": "string", - "label": "model_memory_limit", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 74 - } - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 72 - }, - "initialIsOpen": false - }, { "id": "def-server.AnomaliesTableRecord", "type": "Interface", @@ -2692,48 +2482,6 @@ }, "initialIsOpen": false }, - { - "id": "def-server.ChunkingConfig", - "type": "Interface", - "label": "ChunkingConfig", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.ChunkingConfig.mode", - "type": "CompoundType", - "label": "mode", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 32 - }, - "signature": [ - "\"auto\" | \"off\" | \"manual\"" - ] - }, - { - "tags": [], - "id": "def-server.ChunkingConfig.time_span", - "type": "string", - "label": "time_span", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 33 - }, - "signature": [ - "string | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 31 - }, - "initialIsOpen": false - }, { "id": "def-server.CombinedJob", "type": "Interface", @@ -2771,13 +2519,7 @@ "lineNumber": 20 }, "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.Datafeed", - "text": "Datafeed" - } + "Datafeed" ] } ], @@ -2841,128 +2583,72 @@ "initialIsOpen": false }, { - "id": "def-server.CustomRule", + "id": "def-server.CustomSettings", "type": "Interface", - "label": "CustomRule", + "label": "CustomSettings", "description": [], "tags": [], "children": [ { "tags": [], - "id": "def-server.CustomRule.actions", + "id": "def-server.CustomSettings.custom_urls", "type": "Array", - "label": "actions", + "label": "custom_urls", "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 91 + "lineNumber": 16 }, "signature": [ - "string[]" + { + "pluginId": "ml", + "scope": "common", + "docId": "kibMlPluginApi", + "section": "def-common.UrlConfig", + "text": "UrlConfig" + }, + "[] | undefined" ] }, { "tags": [], - "id": "def-server.CustomRule.scope", - "type": "Uncategorized", - "label": "scope", + "id": "def-server.CustomSettings.created_by", + "type": "CompoundType", + "label": "created_by", "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 92 + "lineNumber": 17 }, "signature": [ - "object | undefined" + { + "pluginId": "ml", + "scope": "common", + "docId": "kibMlPluginApi", + "section": "def-common.CREATED_BY_LABEL", + "text": "CREATED_BY_LABEL" + }, + " | undefined" ] }, { "tags": [], - "id": "def-server.CustomRule.conditions", - "type": "Array", - "label": "conditions", + "id": "def-server.CustomSettings.job_tags", + "type": "Object", + "label": "job_tags", "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 93 + "lineNumber": 18 }, "signature": [ - "any[]" + "{ [tag: string]: string; } | undefined" ] } ], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 90 - }, - "initialIsOpen": false - }, - { - "id": "def-server.CustomSettings", - "type": "Interface", - "label": "CustomSettings", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.CustomSettings.custom_urls", - "type": "Array", - "label": "custom_urls", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 15 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.UrlConfig", - "text": "UrlConfig" - }, - "[] | undefined" - ] - }, - { - "tags": [], - "id": "def-server.CustomSettings.created_by", - "type": "CompoundType", - "label": "created_by", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 16 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.CREATED_BY_LABEL", - "text": "CREATED_BY_LABEL" - }, - " | undefined" - ] - }, - { - "tags": [], - "id": "def-server.CustomSettings.job_tags", - "type": "Object", - "label": "job_tags", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 17 - }, - "signature": [ - "{ [tag: string]: string; } | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 14 + "lineNumber": 15 }, "initialIsOpen": false }, @@ -3182,1065 +2868,223 @@ "initialIsOpen": false }, { - "id": "def-server.DataDescription", - "type": "Interface", - "label": "DataDescription", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.DataDescription.format", - "type": "string", - "label": "format", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 78 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.DataDescription.time_field", - "type": "string", - "label": "time_field", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 79 - } - }, - { - "tags": [], - "id": "def-server.DataDescription.time_format", - "type": "string", - "label": "time_format", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 80 - }, - "signature": [ - "string | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 77 - }, - "initialIsOpen": false - }, - { - "id": "def-server.Datafeed", + "id": "def-server.DatafeedStats", "type": "Interface", - "label": "Datafeed", + "label": "DatafeedStats", "description": [], "tags": [], "children": [ { "tags": [], - "id": "def-server.Datafeed.datafeed_id", + "id": "def-server.DatafeedStats.datafeed_id", "type": "string", "label": "datafeed_id", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 14 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", + "lineNumber": 12 } }, { "tags": [], - "id": "def-server.Datafeed.aggregations", - "type": "Object", - "label": "aggregations", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 15 - }, - "signature": [ - "Record | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Datafeed.aggs", - "type": "Object", - "label": "aggs", + "id": "def-server.DatafeedStats.state", + "type": "Enum", + "label": "state", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 16 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", + "lineNumber": 13 }, "signature": [ - "Record | undefined" + { + "pluginId": "ml", + "scope": "common", + "docId": "kibMlPluginApi", + "section": "def-common.DATAFEED_STATE", + "text": "DATAFEED_STATE" + } ] }, { "tags": [], - "id": "def-server.Datafeed.chunking_config", + "id": "def-server.DatafeedStats.node", "type": "Object", - "label": "chunking_config", + "label": "node", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 17 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", + "lineNumber": 14 }, "signature": [ { "pluginId": "ml", "scope": "common", "docId": "kibMlPluginApi", - "section": "def-common.ChunkingConfig", - "text": "ChunkingConfig" - }, - " | undefined" + "section": "def-common.Node", + "text": "Node" + } ] }, { "tags": [], - "id": "def-server.Datafeed.frequency", + "id": "def-server.DatafeedStats.assignment_explanation", "type": "string", - "label": "frequency", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 18 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Datafeed.indices", - "type": "Array", - "label": "indices", + "label": "assignment_explanation", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 19 - }, - "signature": [ - "string[]" - ] + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", + "lineNumber": 15 + } }, { "tags": [], - "id": "def-server.Datafeed.indexes", - "type": "Array", - "label": "indexes", + "id": "def-server.DatafeedStats.timing_stats", + "type": "Object", + "label": "timing_stats", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 20 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", + "lineNumber": 16 }, "signature": [ - "string[] | undefined" + "TimingStats" ] - }, + } + ], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", + "lineNumber": 11 + }, + "initialIsOpen": false + }, + { + "id": "def-server.ForecastsStats", + "type": "Interface", + "label": "ForecastsStats", + "description": [], + "tags": [], + "children": [ { "tags": [], - "id": "def-server.Datafeed.job_id", - "type": "string", - "label": "job_id", + "id": "def-server.ForecastsStats.total", + "type": "number", + "label": "total", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 21 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 66 } }, { "tags": [], - "id": "def-server.Datafeed.query", - "type": "Uncategorized", - "label": "query", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 22 - }, - "signature": [ - "object" - ] - }, - { - "tags": [], - "id": "def-server.Datafeed.query_delay", - "type": "string", - "label": "query_delay", + "id": "def-server.ForecastsStats.forecasted_jobs", + "type": "number", + "label": "forecasted_jobs", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 23 - }, - "signature": [ - "string | undefined" - ] + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 67 + } }, { "tags": [], - "id": "def-server.Datafeed.script_fields", - "type": "Object", - "label": "script_fields", + "id": "def-server.ForecastsStats.memory_bytes", + "type": "Any", + "label": "memory_bytes", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 24 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 68 }, "signature": [ - "Record | undefined" + "any" ] }, { "tags": [], - "id": "def-server.Datafeed.runtime_mappings", - "type": "Object", - "label": "runtime_mappings", + "id": "def-server.ForecastsStats.records", + "type": "Any", + "label": "records", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 25 - }, - "signature": [ - "Record | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Datafeed.scroll_size", - "type": "number", - "label": "scroll_size", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 26 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Datafeed.delayed_data_check_config", - "type": "Uncategorized", - "label": "delayed_data_check_config", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 27 - }, - "signature": [ - "object | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Datafeed.indices_options", - "type": "Object", - "label": "indices_options", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 28 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.IndicesOptions", - "text": "IndicesOptions" - }, - " | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 13 - }, - "initialIsOpen": false - }, - { - "id": "def-server.DatafeedStats", - "type": "Interface", - "label": "DatafeedStats", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.DatafeedStats.datafeed_id", - "type": "string", - "label": "datafeed_id", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", - "lineNumber": 12 - } - }, - { - "tags": [], - "id": "def-server.DatafeedStats.state", - "type": "Enum", - "label": "state", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", - "lineNumber": 13 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.DATAFEED_STATE", - "text": "DATAFEED_STATE" - } - ] - }, - { - "tags": [], - "id": "def-server.DatafeedStats.node", - "type": "Object", - "label": "node", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", - "lineNumber": 14 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.Node", - "text": "Node" - } - ] - }, - { - "tags": [], - "id": "def-server.DatafeedStats.assignment_explanation", - "type": "string", - "label": "assignment_explanation", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", - "lineNumber": 15 - } - }, - { - "tags": [], - "id": "def-server.DatafeedStats.timing_stats", - "type": "Object", - "label": "timing_stats", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", - "lineNumber": 16 - }, - "signature": [ - "TimingStats" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts", - "lineNumber": 11 - }, - "initialIsOpen": false - }, - { - "id": "def-server.Detector", - "type": "Interface", - "label": "Detector", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.Detector.by_field_name", - "type": "string", - "label": "by_field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 61 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.detector_description", - "type": "string", - "label": "detector_description", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 62 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.detector_index", - "type": "number", - "label": "detector_index", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 63 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.exclude_frequent", - "type": "string", - "label": "exclude_frequent", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 64 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.field_name", - "type": "string", - "label": "field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 65 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.function", - "type": "string", - "label": "function", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 66 - } - }, - { - "tags": [], - "id": "def-server.Detector.over_field_name", - "type": "string", - "label": "over_field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 67 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.partition_field_name", - "type": "string", - "label": "partition_field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 68 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.use_null", - "type": "CompoundType", - "label": "use_null", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 69 - }, - "signature": [ - "boolean | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Detector.custom_rules", - "type": "Array", - "label": "custom_rules", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 70 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.CustomRule", - "text": "CustomRule" - }, - "[] | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 60 - }, - "initialIsOpen": false - }, - { - "id": "def-server.ForecastsStats", - "type": "Interface", - "label": "ForecastsStats", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.ForecastsStats.total", - "type": "number", - "label": "total", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 66 - } - }, - { - "tags": [], - "id": "def-server.ForecastsStats.forecasted_jobs", - "type": "number", - "label": "forecasted_jobs", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 67 - } - }, - { - "tags": [], - "id": "def-server.ForecastsStats.memory_bytes", - "type": "Any", - "label": "memory_bytes", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 68 - }, - "signature": [ - "any" - ] - }, - { - "tags": [], - "id": "def-server.ForecastsStats.records", - "type": "Any", - "label": "records", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 69 - }, - "signature": [ - "any" - ] - }, - { - "tags": [], - "id": "def-server.ForecastsStats.processing_time_ms", - "type": "Any", - "label": "processing_time_ms", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 70 - }, - "signature": [ - "any" - ] - }, - { - "tags": [], - "id": "def-server.ForecastsStats.status", - "type": "Any", - "label": "status", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 71 - }, - "signature": [ - "any" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", - "lineNumber": 65 - }, - "initialIsOpen": false - }, - { - "id": "def-server.IndicesOptions", - "type": "Interface", - "label": "IndicesOptions", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.IndicesOptions.expand_wildcards", - "type": "CompoundType", - "label": "expand_wildcards", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 49 - }, - "signature": [ - "\"all\" | \"none\" | \"hidden\" | \"open\" | \"closed\" | undefined" - ] - }, - { - "tags": [], - "id": "def-server.IndicesOptions.ignore_unavailable", - "type": "CompoundType", - "label": "ignore_unavailable", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 50 - }, - "signature": [ - "boolean | undefined" - ] - }, - { - "tags": [], - "id": "def-server.IndicesOptions.allow_no_indices", - "type": "CompoundType", - "label": "allow_no_indices", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 51 - }, - "signature": [ - "boolean | undefined" - ] - }, - { - "tags": [], - "id": "def-server.IndicesOptions.ignore_throttled", - "type": "CompoundType", - "label": "ignore_throttled", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 52 - }, - "signature": [ - "boolean | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 48 - }, - "initialIsOpen": false - }, - { - "id": "def-server.Influencer", - "type": "Interface", - "label": "Influencer", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.Influencer.influencer_field_name", - "type": "string", - "label": "influencer_field_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomalies.ts", - "lineNumber": 11 - } - }, - { - "tags": [], - "id": "def-server.Influencer.influencer_field_values", - "type": "Array", - "label": "influencer_field_values", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomalies.ts", - "lineNumber": 12 - }, - "signature": [ - "string[]" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomalies.ts", - "lineNumber": 10 - }, - "initialIsOpen": false - }, - { - "id": "def-server.Job", - "type": "Interface", - "label": "Job", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.Job.job_id", - "type": "string", - "label": "job_id", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 23 - } - }, - { - "tags": [], - "id": "def-server.Job.analysis_config", - "type": "Object", - "label": "analysis_config", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 24 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.AnalysisConfig", - "text": "AnalysisConfig" - } - ] - }, - { - "tags": [], - "id": "def-server.Job.analysis_limits", - "type": "Object", - "label": "analysis_limits", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 25 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.AnalysisLimits", - "text": "AnalysisLimits" - }, - " | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.background_persist_interval", - "type": "string", - "label": "background_persist_interval", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 26 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.custom_settings", - "type": "Object", - "label": "custom_settings", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 27 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.CustomSettings", - "text": "CustomSettings" - }, - " | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.data_description", - "type": "Object", - "label": "data_description", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 28 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.DataDescription", - "text": "DataDescription" - } - ] - }, - { - "tags": [], - "id": "def-server.Job.description", - "type": "string", - "label": "description", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 29 - } - }, - { - "tags": [], - "id": "def-server.Job.groups", - "type": "Array", - "label": "groups", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 30 - }, - "signature": [ - "string[]" - ] - }, - { - "tags": [], - "id": "def-server.Job.model_plot_config", - "type": "Object", - "label": "model_plot_config", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 31 - }, - "signature": [ - { - "pluginId": "ml", - "scope": "common", - "docId": "kibMlPluginApi", - "section": "def-common.ModelPlotConfig", - "text": "ModelPlotConfig" - }, - " | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.model_snapshot_retention_days", - "type": "number", - "label": "model_snapshot_retention_days", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 32 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.daily_model_snapshot_retention_after_days", - "type": "number", - "label": "daily_model_snapshot_retention_after_days", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 33 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.renormalization_window_days", - "type": "number", - "label": "renormalization_window_days", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 34 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.results_index_name", - "type": "string", - "label": "results_index_name", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 35 - }, - "signature": [ - "string | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.results_retention_days", - "type": "number", - "label": "results_retention_days", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 36 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.create_time", - "type": "number", - "label": "create_time", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 39 - }, - "signature": [ - "number | undefined" - ] - }, - { - "tags": [], - "id": "def-server.Job.finished_time", - "type": "number", - "label": "finished_time", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 40 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 69 }, "signature": [ - "number | undefined" + "any" ] }, { "tags": [], - "id": "def-server.Job.job_type", - "type": "string", - "label": "job_type", + "id": "def-server.ForecastsStats.processing_time_ms", + "type": "Any", + "label": "processing_time_ms", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 41 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 70 }, "signature": [ - "\"anomaly_detector\" | undefined" + "any" ] }, { "tags": [], - "id": "def-server.Job.job_version", - "type": "string", - "label": "job_version", + "id": "def-server.ForecastsStats.status", + "type": "Any", + "label": "status", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 42 + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 71 }, "signature": [ - "string | undefined" + "any" ] - }, + } + ], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts", + "lineNumber": 65 + }, + "initialIsOpen": false + }, + { + "id": "def-server.Influencer", + "type": "Interface", + "label": "Influencer", + "description": [], + "tags": [], + "children": [ { "tags": [], - "id": "def-server.Job.model_snapshot_id", + "id": "def-server.Influencer.influencer_field_name", "type": "string", - "label": "model_snapshot_id", + "label": "influencer_field_name", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 43 - }, - "signature": [ - "string | undefined" - ] + "path": "x-pack/plugins/ml/common/types/anomalies.ts", + "lineNumber": 11 + } }, { "tags": [], - "id": "def-server.Job.deleting", - "type": "CompoundType", - "label": "deleting", + "id": "def-server.Influencer.influencer_field_values", + "type": "Array", + "label": "influencer_field_values", "description": [], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 44 + "path": "x-pack/plugins/ml/common/types/anomalies.ts", + "lineNumber": 12 }, "signature": [ - "boolean | undefined" + "string[]" ] } ], "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 22 + "path": "x-pack/plugins/ml/common/types/anomalies.ts", + "lineNumber": 10 }, "initialIsOpen": false }, @@ -4790,62 +3634,6 @@ }, "initialIsOpen": false }, - { - "id": "def-server.ModelPlotConfig", - "type": "Interface", - "label": "ModelPlotConfig", - "description": [], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.ModelPlotConfig.enabled", - "type": "CompoundType", - "label": "enabled", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 84 - }, - "signature": [ - "boolean | undefined" - ] - }, - { - "tags": [], - "id": "def-server.ModelPlotConfig.annotations_enabled", - "type": "CompoundType", - "label": "annotations_enabled", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 85 - }, - "signature": [ - "boolean | undefined" - ] - }, - { - "tags": [], - "id": "def-server.ModelPlotConfig.terms", - "type": "string", - "label": "terms", - "description": [], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 86 - }, - "signature": [ - "string | undefined" - ] - } - ], - "source": { - "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 83 - }, - "initialIsOpen": false - }, { "id": "def-server.ModelSizeStats", "type": "Interface", @@ -5298,7 +4086,7 @@ "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 97 + "lineNumber": 106 }, "signature": [ "boolean | undefined" @@ -5312,7 +4100,7 @@ "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 98 + "lineNumber": 107 }, "signature": [ "boolean | undefined" @@ -5321,7 +4109,7 @@ ], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 96 + "lineNumber": 105 }, "initialIsOpen": false } @@ -5336,13 +4124,43 @@ "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 36 + "lineNumber": 44 }, "signature": [ "{ [x: string]: { date_histogram: { field: string; fixed_interval: string;}; aggregations?: { [key: string]: any; } | undefined; aggs?: { [key: string]: any; } | undefined; }; }" ], "initialIsOpen": false }, + { + "id": "def-server.AnalysisConfig", + "type": "Type", + "label": "AnalysisConfig", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 49 + }, + "signature": [ + "estypes.AnalysisConfig" + ], + "initialIsOpen": false + }, + { + "id": "def-server.AnalysisLimits", + "type": "Type", + "label": "AnalysisLimits", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 77 + }, + "signature": [ + "estypes.AnalysisLimits" + ], + "initialIsOpen": false + }, { "id": "def-server.AnomalyResultType", "type": "Type", @@ -5366,13 +4184,73 @@ "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 12 + "lineNumber": 13 }, "signature": [ "string" ], "initialIsOpen": false }, + { + "id": "def-server.ChunkingConfig", + "type": "Type", + "label": "ChunkingConfig", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", + "lineNumber": 37 + }, + "signature": [ + "estypes.ChunkingConfig" + ], + "initialIsOpen": false + }, + { + "id": "def-server.CustomRule", + "type": "Type", + "label": "CustomRule", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 97 + }, + "signature": [ + "estypes.DetectionRule" + ], + "initialIsOpen": false + }, + { + "id": "def-server.DataDescription", + "type": "Type", + "label": "DataDescription", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 83 + }, + "signature": [ + "estypes.DataDescription" + ], + "initialIsOpen": false + }, + { + "id": "def-server.Datafeed", + "type": "Type", + "label": "Datafeed", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", + "lineNumber": 14 + }, + "signature": [ + "estypes.Datafeed" + ], + "initialIsOpen": false + }, { "id": "def-server.DatafeedId", "type": "Type", @@ -5381,7 +4259,7 @@ "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", - "lineNumber": 11 + "lineNumber": 12 }, "signature": [ "string" @@ -5399,7 +4277,23 @@ "lineNumber": 14 }, "signature": [ - "Datafeed & DatafeedStats" + "Datafeed", + " & DatafeedStats" + ], + "initialIsOpen": false + }, + { + "id": "def-server.Detector", + "type": "Type", + "label": "Detector", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 63 + }, + "signature": [ + "estypes.Detector" ], "initialIsOpen": false }, @@ -5418,6 +4312,36 @@ ], "initialIsOpen": false }, + { + "id": "def-server.IndicesOptions", + "type": "Type", + "label": "IndicesOptions", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts", + "lineNumber": 56 + }, + "signature": [ + "estypes.IndicesOptions" + ], + "initialIsOpen": false + }, + { + "id": "def-server.Job", + "type": "Type", + "label": "Job", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 23 + }, + "signature": [ + "estypes.Job" + ], + "initialIsOpen": false + }, { "id": "def-server.JobId", "type": "Type", @@ -5426,7 +4350,7 @@ "description": [], "source": { "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", - "lineNumber": 11 + "lineNumber": 12 }, "signature": [ "string" @@ -5444,7 +4368,8 @@ "lineNumber": 13 }, "signature": [ - "Job & JobStats" + "Job", + " & JobStats" ], "initialIsOpen": false }, @@ -5463,6 +4388,21 @@ ], "initialIsOpen": false }, + { + "id": "def-server.ModelPlotConfig", + "type": "Type", + "label": "ModelPlotConfig", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts", + "lineNumber": 90 + }, + "signature": [ + "estypes.ModelPlotConfig" + ], + "initialIsOpen": false + }, { "id": "def-server.ModuleSetupPayload", "type": "Type", diff --git a/api_docs/observability.json b/api_docs/observability.json index 439fd18db6469..2128e27f0106f 100644 --- a/api_docs/observability.json +++ b/api_docs/observability.json @@ -474,7 +474,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/public/hooks/use_fetcher.tsx", - "lineNumber": 30 + "lineNumber": 31 } }, { @@ -487,7 +487,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/public/hooks/use_fetcher.tsx", - "lineNumber": 31 + "lineNumber": 32 } }, { @@ -505,7 +505,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/public/hooks/use_fetcher.tsx", - "lineNumber": 33 + "lineNumber": 34 }, "signature": [ "boolean | undefined" @@ -514,7 +514,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/hooks/use_fetcher.tsx", - "lineNumber": 32 + "lineNumber": 33 } } ], @@ -522,7 +522,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/observability/public/hooks/use_fetcher.tsx", - "lineNumber": 29 + "lineNumber": 30 }, "initialIsOpen": false }, @@ -1359,6 +1359,136 @@ }, "initialIsOpen": false }, + { + "id": "def-public.ObservabilityPublicPluginsSetup", + "type": "Interface", + "label": "ObservabilityPublicPluginsSetup", + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-public.ObservabilityPublicPluginsSetup.data", + "type": "Object", + "label": "data", + "description": [], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 30 + }, + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginSetup", + "text": "DataPublicPluginSetup" + } + ] + }, + { + "tags": [], + "id": "def-public.ObservabilityPublicPluginsSetup.home", + "type": "Object", + "label": "home", + "description": [], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 31 + }, + "signature": [ + { + "pluginId": "home", + "scope": "public", + "docId": "kibHomePluginApi", + "section": "def-public.HomePublicPluginSetup", + "text": "HomePublicPluginSetup" + }, + " | undefined" + ] + } + ], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 29 + }, + "initialIsOpen": false + }, + { + "id": "def-public.ObservabilityPublicPluginsStart", + "type": "Interface", + "label": "ObservabilityPublicPluginsStart", + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-public.ObservabilityPublicPluginsStart.home", + "type": "Object", + "label": "home", + "description": [], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 35 + }, + "signature": [ + { + "pluginId": "home", + "scope": "public", + "docId": "kibHomePluginApi", + "section": "def-public.HomePublicPluginStart", + "text": "HomePublicPluginStart" + }, + " | undefined" + ] + }, + { + "tags": [], + "id": "def-public.ObservabilityPublicPluginsStart.data", + "type": "Object", + "label": "data", + "description": [], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 36 + }, + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + } + ] + }, + { + "tags": [], + "id": "def-public.ObservabilityPublicPluginsStart.lens", + "type": "Object", + "label": "lens", + "description": [], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 37 + }, + "signature": [ + { + "pluginId": "lens", + "scope": "public", + "docId": "kibLensPluginApi", + "section": "def-public.LensPublicStart", + "text": "LensPublicStart" + } + ] + } + ], + "source": { + "path": "x-pack/plugins/observability/public/plugin.ts", + "lineNumber": 34 + }, + "initialIsOpen": false + }, { "id": "def-public.Series", "type": "Interface", @@ -1951,21 +2081,21 @@ ], "objects": [], "setup": { - "id": "def-public.ObservabilityPluginSetup", + "id": "def-public.ObservabilityPublicSetup", "type": "Interface", - "label": "ObservabilityPluginSetup", + "label": "ObservabilityPublicSetup", "description": [], "tags": [], "children": [ { "tags": [], - "id": "def-public.ObservabilityPluginSetup.dashboard", + "id": "def-public.ObservabilityPublicSetup.dashboard", "type": "Object", "label": "dashboard", "description": [], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 25 + "lineNumber": 26 }, "signature": [ "{ register: typeof ", @@ -1982,20 +2112,20 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 24 + "lineNumber": 25 }, "lifecycle": "setup", "initialIsOpen": true }, "start": { - "id": "def-public.ObservabilityPluginStart", + "id": "def-public.ObservabilityPublicStart", "type": "Type", - "label": "ObservabilityPluginStart", + "label": "ObservabilityPublicStart", "tags": [], "description": [], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 33 + "lineNumber": 40 }, "signature": [ "void" @@ -2086,8 +2216,8 @@ "pluginId": "observability", "scope": "server", "docId": "kibObservabilityPluginApi", - "section": "def-server.MappingsDefinition", - "text": "MappingsDefinition" + "section": "def-server.Mappings", + "text": "Mappings" }, "; client: ", { @@ -2118,26 +2248,26 @@ "description": [], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 32 + "lineNumber": 20 } }, { "tags": [], "id": "def-server.createOrUpdateIndex.{\n- index,\n mappings,\n client,\n logger,\n}.mappings", - "type": "Object", + "type": "CompoundType", "label": "mappings", "description": [], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 33 + "lineNumber": 21 }, "signature": [ { "pluginId": "observability", "scope": "server", "docId": "kibObservabilityPluginApi", - "section": "def-server.MappingsDefinition", - "text": "MappingsDefinition" + "section": "def-server.Mappings", + "text": "Mappings" } ] }, @@ -2149,7 +2279,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 34 + "lineNumber": 22 }, "signature": [ { @@ -2169,7 +2299,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 35 + "lineNumber": 23 }, "signature": [ "Logger" @@ -2178,7 +2308,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 31 + "lineNumber": 19 } } ], @@ -2186,7 +2316,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 26 + "lineNumber": 14 }, "initialIsOpen": false }, @@ -2224,82 +2354,32 @@ "initialIsOpen": false } ], - "interfaces": [ + "interfaces": [], + "enums": [], + "misc": [ { - "id": "def-server.MappingsDefinition", - "type": "Interface", - "label": "MappingsDefinition", - "description": [], + "id": "def-server.Mappings", + "type": "Type", + "label": "Mappings", "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.MappingsDefinition.dynamic", - "type": "CompoundType", - "label": "dynamic", - "description": [], - "source": { - "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 21 - }, - "signature": [ - "boolean | \"strict\" | undefined" - ] - }, - { - "tags": [], - "id": "def-server.MappingsDefinition.properties", - "type": "Object", - "label": "properties", - "description": [], - "source": { - "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 22 - }, - "signature": [ - "Record" - ] - }, - { - "tags": [], - "id": "def-server.MappingsDefinition.dynamic_templates", - "type": "Array", - "label": "dynamic_templates", - "description": [], - "source": { - "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 23 - }, - "signature": [ - "any[] | undefined" - ] - } - ], + "description": [], "source": { "path": "x-pack/plugins/observability/server/utils/create_or_update_index.ts", - "lineNumber": 20 + "lineNumber": 11 }, + "signature": [ + "TypeMapping", + " & { all_field?: ", + "AllField", + " | undefined; date_detection?: boolean | undefined; dynamic?: boolean | \"strict\" | undefined; dynamic_date_formats?: string[] | undefined; dynamic_templates?: Record | Record[] | undefined; field_names_field?: ", + "FieldNamesField" + ], "initialIsOpen": false - } - ], - "enums": [], - "misc": [ + }, { "id": "def-server.ObservabilityConfig", "type": "Type", @@ -2330,7 +2410,9 @@ "Annotation", "; }>; getById: (getByIdParams: { id: string; }) => Promise<", "GetResponse", - ">; delete: (deleteParams: { id: string; }) => Promise>; }" + ">; delete: (deleteParams: { id: string; }) => Promise<", + "DeleteResponse", + ">; }" ], "initialIsOpen": false } diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 353e65b0fa080..ef0e952f5161c 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -42,9 +42,6 @@ import observabilityObj from './observability.json'; ### Classes -### Interfaces - - ### Consts, variables and types diff --git a/api_docs/reporting.json b/api_docs/reporting.json index 29d0d485452da..be0c963866038 100644 --- a/api_docs/reporting.json +++ b/api_docs/reporting.json @@ -1352,7 +1352,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">) => Promise<", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">) => Promise<", { "pluginId": "core", "scope": "server", @@ -1377,7 +1377,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "description": [], "source": { diff --git a/api_docs/security.json b/api_docs/security.json index 8e1214654a82f..938a043a555cf 100644 --- a/api_docs/security.json +++ b/api_docs/security.json @@ -982,7 +982,7 @@ "lineNumber": 109 }, "signature": [ - "{ type: string; reason: string; caused_by: { type: string; reason: string; }; }[] | undefined" + "{ type: string; reason: string; caused_by?: { type: string; reason: string; } | undefined; }[] | undefined" ] } ], diff --git a/api_docs/security_solution.json b/api_docs/security_solution.json index ae208eb4facc7..ae85541ba6bfd 100644 --- a/api_docs/security_solution.json +++ b/api_docs/security_solution.json @@ -52,7 +52,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 75 + "lineNumber": 79 } } ], @@ -60,7 +60,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 75 + "lineNumber": 79 } }, { @@ -144,7 +144,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 98 + "lineNumber": 103 } }, { @@ -163,7 +163,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 98 + "lineNumber": 103 } } ], @@ -171,7 +171,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 98 + "lineNumber": 103 } }, { @@ -215,7 +215,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 348 + "lineNumber": 353 } }, { @@ -234,7 +234,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 348 + "lineNumber": 353 } } ], @@ -242,7 +242,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 348 + "lineNumber": 353 } }, { @@ -258,13 +258,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 393 + "lineNumber": 398 } } ], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 72 + "lineNumber": 75 }, "initialIsOpen": false } @@ -607,7 +607,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 338 + "lineNumber": 339 } }, { @@ -626,7 +626,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 338 + "lineNumber": 339 } } ], @@ -634,7 +634,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 338 + "lineNumber": 339 } }, { @@ -650,7 +650,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 404 + "lineNumber": 405 } } ], @@ -1415,9 +1415,7 @@ "signature": [ "Error & Partial<", "ResponseError", - ", ", - "Context", - ">>" + ", unknown>>" ], "description": [], "source": { @@ -1429,9 +1427,7 @@ "signature": [ "(err: Error & Partial<", "ResponseError", - ", ", - "Context", - ">>) => ", + ", unknown>>) => ", "OutputError" ], "description": [], diff --git a/api_docs/telemetry.json b/api_docs/telemetry.json index 2d0108158e7e3..043e126de3640 100644 --- a/api_docs/telemetry.json +++ b/api_docs/telemetry.json @@ -334,7 +334,7 @@ "signature": [ "({ esClient }: ", "StatsCollectionConfig", - ") => Promise<{ clusterUuid: any; }[]>" + ") => Promise<{ clusterUuid: string; }[]>" ], "description": [ "\nGet the cluster uuids from the connected cluster." @@ -369,7 +369,7 @@ "description": [], "source": { "path": "src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts", - "lineNumber": 59 + "lineNumber": 60 } }, { @@ -388,7 +388,7 @@ "description": [], "source": { "path": "src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts", - "lineNumber": 60 + "lineNumber": 61 } }, { @@ -407,7 +407,7 @@ "description": [], "source": { "path": "src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts", - "lineNumber": 61 + "lineNumber": 62 } } ], @@ -434,7 +434,7 @@ "label": "getLocalStats", "source": { "path": "src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts", - "lineNumber": 58 + "lineNumber": 59 }, "tags": [], "returnComment": [], @@ -453,7 +453,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">, uiSettingsClient: ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">, uiSettingsClient: ", { "pluginId": "core", "scope": "server", @@ -478,7 +478,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "description": [], "source": { @@ -694,7 +694,7 @@ "description": [], "source": { "path": "src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts", - "lineNumber": 50 + "lineNumber": 51 }, "signature": [ "{ timestamp: string; cluster_uuid: string; cluster_name: string; version: string; cluster_stats: any; collection: string; stack_stats: { data: DataTelemetryPayload | undefined; kibana: { count: number; indices: number; os: {}; versions: { version: string; count: number; }[]; plugins: { [plugin: string]: any; }; } | undefined; }; }" diff --git a/api_docs/telemetry_collection_manager.json b/api_docs/telemetry_collection_manager.json index 09e4b884bf809..30d3bb76a43b2 100644 --- a/api_docs/telemetry_collection_manager.json +++ b/api_docs/telemetry_collection_manager.json @@ -105,7 +105,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ] }, { diff --git a/api_docs/telemetry_collection_xpack.json b/api_docs/telemetry_collection_xpack.json index cf1b1a5998553..8110cd6f2d27e 100644 --- a/api_docs/telemetry_collection_xpack.json +++ b/api_docs/telemetry_collection_xpack.json @@ -11,167 +11,25 @@ "server": { "classes": [], "functions": [], - "interfaces": [ + "interfaces": [], + "enums": [], + "misc": [ { "id": "def-server.ESLicense", - "type": "Interface", + "type": "Type", "label": "ESLicense", - "description": [], "tags": [], - "children": [ - { - "tags": [], - "id": "def-server.ESLicense.status", - "type": "string", - "label": "status", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 12 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.uid", - "type": "string", - "label": "uid", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 13 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.hkey", - "type": "string", - "label": "hkey", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 14 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.type", - "type": "string", - "label": "type", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 15 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.issue_date", - "type": "string", - "label": "issue_date", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 16 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.issue_date_in_millis", - "type": "number", - "label": "issue_date_in_millis", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 17 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.expiry_date", - "type": "string", - "label": "expiry_date", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 18 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.expiry_date_in_millis", - "type": "number", - "label": "expiry_date_in_millis", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 19 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.max_nodes", - "type": "number", - "label": "max_nodes", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 20 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.issued_to", - "type": "string", - "label": "issued_to", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 21 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.issuer", - "type": "string", - "label": "issuer", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 22 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.start_date_in_millis", - "type": "number", - "label": "start_date_in_millis", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 23 - } - }, - { - "tags": [], - "id": "def-server.ESLicense.max_resource_units", - "type": "number", - "label": "max_resource_units", - "description": [], - "source": { - "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 24 - } - } - ], + "description": [], "source": { "path": "x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts", - "lineNumber": 11 + "lineNumber": 10 }, + "signature": [ + "estypes.LicenseInformation" + ], "initialIsOpen": false } ], - "enums": [], - "misc": [], "objects": [] }, "common": { diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index dbebc1cde59ab..058a9d3fcb460 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -13,6 +13,6 @@ import telemetryCollectionXpackObj from './telemetry_collection_xpack.json'; ## Server -### Interfaces - +### Consts, variables and types + diff --git a/api_docs/vis_type_timeseries.json b/api_docs/vis_type_timeseries.json index 6f349cb3dfb93..907ced500294a 100644 --- a/api_docs/vis_type_timeseries.json +++ b/api_docs/vis_type_timeseries.json @@ -37,7 +37,7 @@ { "pluginId": "data", "scope": "server", - "docId": "kibDataPluginApi", + "docId": "kibDataSearchPluginApi", "section": "def-server.DataRequestHandlerContext", "text": "DataRequestHandlerContext" }, diff --git a/dev_docs/assets/api_doc_pick.png b/dev_docs/assets/api_doc_pick.png new file mode 100644 index 0000000000000..825fa47b266cb Binary files /dev/null and b/dev_docs/assets/api_doc_pick.png differ diff --git a/dev_docs/assets/dev_docs_nested_object.png b/dev_docs/assets/dev_docs_nested_object.png new file mode 100644 index 0000000000000..a6b2f533b3858 Binary files /dev/null and b/dev_docs/assets/dev_docs_nested_object.png differ diff --git a/dev_docs/best_practices.mdx b/dev_docs/best_practices.mdx index 6156c05197289..4d51263f93372 100644 --- a/dev_docs/best_practices.mdx +++ b/dev_docs/best_practices.mdx @@ -12,6 +12,132 @@ tags: ['kibana', 'onboarding', 'dev', 'architecture'] First things first, be sure to review our and check out all the available platform that can simplify plugin development. +## Developer documentation + +### High-level documentation + +#### Structure + +Refer to [divio documentation](https://documentation.divio.com/) for guidance on where and how to structure our high-level documentation. + + and + sections are both _explanation_ oriented, + covers both _tutorials_ and _How to_, and +the section covers _reference_ material. + +#### Location + +If the information spans multiple plugins, consider adding it to the [dev_docs](https://github.com/elastic/kibana/tree/master/dev_docs) folder. If it is plugin specific, consider adding it inside the plugin folder. Write it in an mdx file if you would like it to show up in our new (beta) documentation system. + + + +To add docs into the new docs system, create an `.mdx` file that +contains . Read about the syntax . An extra step is needed to add a menu item. will walk you through how to set the docs system +up locally and edit the nav menu. + + + +#### Keep content fresh + +A fresh pair of eyes are invaluable. Recruit new hires to read, review and update documentation. Leads should also periodically review documentation to ensure it stays up to date. File issues any time you notice documentation is outdated. + +#### Consider your target audience + +Documentation in the Kibana Developer Guide is targeted towards developers building Kibana plugins. Keep implementation details about internal plugin code out of these docs. + +#### High to low level + +When a developer first lands in our docs, think about their journey. Introduce basic concepts before diving into details. The left navigation should be set up so documents on top are higher level than documents near the bottom. + +#### Think outside-in + +It's easy to forget what it felt like to first write code in Kibana, but do your best to frame these docs "outside-in". Don't use esoteric, internal language unless a definition is documented and linked. The fresh eyes of a new hire can be a great asset. + +### API documentation + +We automatically generate . The following guidelines will help ensure your are useful. + +#### Code comments + +Every publicly exposed function, class, interface, type, parameter and property should have a comment using JSDoc style comments. + +- Use `@param` tags for every function parameter. +- Use `@returns` tags for return types. +- Use `@throws` when appropriate. +- Use `@beta` or `@deprecated` when appropriate. +- Use `@internal` to indicate this API item is intended for internal use only, which will also remove it from the docs. + +#### Interfaces vs inlined types + +Prefer types and interfaces over complex inline objects. For example, prefer: + +```ts +/** +* The SearchSpec interface contains settings for creating a new SearchService, like +* username and password. +*/ +export interface SearchSpec { + /** + * Stores the username. Duh, + */ + username: string; + /** + * Stores the password. I hope it's encrypted! + */ + password: string; +} + + /** + * Retrieve search services + * @param searchSpec Configuration information for initializing the search service. + * @returns the id of the search service + */ +export getSearchService: (searchSpec: SearchSpec) => string; +``` + +over: + +```ts +/** + * Retrieve search services + * @param searchSpec Configuration information for initializing the search service. + * @returns the id of the search service + */ +export getSearchService: (searchSpec: { username: string; password: string }) => string; +``` + +In the former, there will be a link to the `SearchSpec` interface with documentation for the `username` and `password` properties. In the latter the object will render inline, without comments: + +![prefer interfaces documentation](./assets/dev_docs_nested_object.png) + +#### Export every type used in a public API + +When a publicly exported API items references a private type, this results in a broken link in our docs system. The private type is, by proxy, part of your public API, and as such, should be exported. + +Do: + +```ts +export interface AnInterface { bar: string }; +export type foo: string | AnInterface; +``` + +Don't: + +```ts +interface AnInterface { bar: string }; +export type foo: string | AnInterface; +``` + +#### Avoid “Pick” + +`Pick` not only ends up being unhelpful in our documentation system, but it's also of limited help in your IDE. For that reason, avoid `Pick` and other similarly complex types on your public API items. Using these semantics internally is fine. + +![pick api documentation](./assets/api_doc_pick.png) + +### Example plugins + +Running Kibana with `yarn start --run-examples` will include all [example plugins](https://github.com/elastic/kibana/tree/master/examples). These are tested examples of platform services in use. We strongly encourage anyone providing a platform level service or to include a tutorial that links to a tested example plugin. This is better than relying on copied code snippets, which can quickly get out of date. + ## Performance Build with scalability in mind. diff --git a/docs/api/actions-and-connectors.asciidoc b/docs/api/actions-and-connectors.asciidoc index 5480cdd57f691..ff4cb8401091e 100644 --- a/docs/api/actions-and-connectors.asciidoc +++ b/docs/api/actions-and-connectors.asciidoc @@ -5,19 +5,19 @@ Manage Actions and Connectors. The following connector APIs are available: -* <> to retrieve a single connector by ID +* <> to retrieve a single connector by ID -* <> to retrieve all connectors +* <> to retrieve all connectors -* <> to retrieve a list of all connector types +* <> to retrieve a list of all connector types -* <> to create connectors +* <> to create connectors -* <> to update the attributes for an existing connector +* <> to update the attributes for an existing connector -* <> to execute a connector by ID +* <> to execute a connector by ID -* <> to delete a connector by ID +* <> to delete a connector by ID For deprecated APIs, refer to <>. diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index c9a09e890ea6d..554e84615d568 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -1,25 +1,25 @@ -[[actions-and-connectors-api-create]] +[[create-connector-api]] === Create connector API ++++ -Create connector API +Create connector ++++ Creates a connector. -[[actions-and-connectors-api-create-request]] +[[create-connector-api-request]] ==== Request `POST :/api/actions/connector` `POST :/s//api/actions/connector` -[[actions-and-connectors-api-create-path-params]] +[[create-connector-api-path-params]] ==== Path parameters `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-create-request-body]] +[[create-connector-api-request-body]] ==== Request body `name`:: @@ -36,15 +36,15 @@ Creates a connector. (Required, object) The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. For information about the secrets configuration properties, refer to <>. + -WARNING: Remember these values. You must provide them each time you call the <> API. +WARNING: Remember these values. You must provide them each time you call the <> API. -[[actions-and-connectors-api-create-request-codes]] +[[create-connector-api-request-codes]] ==== Response code `200`:: Indicates a successful call. -[[actions-and-connectors-api-create-example]] +[[create-connector-api-example]] ==== Example [source,sh] diff --git a/docs/api/actions-and-connectors/delete.asciidoc b/docs/api/actions-and-connectors/delete.asciidoc index a9f9e658613e0..021a3f7cdf3f7 100644 --- a/docs/api/actions-and-connectors/delete.asciidoc +++ b/docs/api/actions-and-connectors/delete.asciidoc @@ -1,21 +1,21 @@ -[[actions-and-connectors-api-delete]] +[[delete-connector-api]] === Delete connector API ++++ -Delete connector API +Delete connector ++++ Deletes an connector by ID. WARNING: When you delete a connector, _it cannot be recovered_. -[[actions-and-connectors-api-delete-request]] +[[delete-connector-api-request]] ==== Request `DELETE :/api/actions/connector/` `DELETE :/s//api/actions/connector/` -[[actions-and-connectors-api-delete-path-params]] +[[delete-connector-api-path-params]] ==== Path parameters `id`:: @@ -24,7 +24,7 @@ WARNING: When you delete a connector, _it cannot be recovered_. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-delete-response-codes]] +[[delete-connector-api-response-codes]] ==== Response code `200`:: diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index b87380907f7bb..e830c9b4bbf88 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -1,19 +1,19 @@ -[[actions-and-connectors-api-execute]] +[[execute-connector-api]] === Execute connector API ++++ -Execute connector API +Execute connector ++++ Executes a connector by ID. -[[actions-and-connectors-api-execute-request]] +[[execute-connector-api-request]] ==== Request `POST :/api/actions/connector//_execute` `POST :/s//api/actions/connector//_execute` -[[actions-and-connectors-api-execute-params]] +[[execute-connector-api-params]] ==== Path parameters `id`:: @@ -22,20 +22,20 @@ Executes a connector by ID. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-execute-request-body]] +[[execute-connector-api-request-body]] ==== Request body `params`:: (Required, object) The parameters of the connector. Parameter properties vary depending on the connector type. For information about the parameter properties, refer to <>. -[[actions-and-connectors-api-execute-codes]] +[[execute-connector-api-codes]] ==== Response code `200`:: Indicates a successful call. -[[actions-and-connectors-api-execute-example]] +[[execute-connector-api-example]] ==== Example [source,sh] diff --git a/docs/api/actions-and-connectors/get.asciidoc b/docs/api/actions-and-connectors/get.asciidoc index 33d37a4add4dd..0d9af45c4ef0c 100644 --- a/docs/api/actions-and-connectors/get.asciidoc +++ b/docs/api/actions-and-connectors/get.asciidoc @@ -1,19 +1,19 @@ -[[actions-and-connectors-api-get]] +[[get-connector-api]] === Get connector API ++++ -Get connector API +Get connector ++++ Retrieves a connector by ID. -[[actions-and-connectors-api-get-request]] +[[get-connector-api-request]] ==== Request `GET :/api/actions/connector/` `GET :/s//api/actions/connector/` -[[actions-and-connectors-api-get-params]] +[[get-connector-api-params]] ==== Path parameters `id`:: @@ -22,13 +22,13 @@ Retrieves a connector by ID. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-get-codes]] +[[get-connector-api-codes]] ==== Response code `200`:: Indicates a successful call. -[[actions-and-connectors-api-get-example]] +[[get-connector-api-example]] ==== Example [source,sh] diff --git a/docs/api/actions-and-connectors/get_all.asciidoc b/docs/api/actions-and-connectors/get_all.asciidoc index 8b4977d61e741..e4e67a9bbde73 100644 --- a/docs/api/actions-and-connectors/get_all.asciidoc +++ b/docs/api/actions-and-connectors/get_all.asciidoc @@ -1,31 +1,31 @@ -[[actions-and-connectors-api-get-all]] -=== Get all actions API +[[get-all-connectors-api]] +=== Get all connectors API ++++ -Get all actions API +Get all connectors ++++ Retrieves all connectors. -[[actions-and-connectors-api-get-all-request]] +[[get-all-connectors-api-request]] ==== Request `GET :/api/actions/connectors` `GET :/s//api/actions/connectors` -[[actions-and-connectors-api-get-all-path-params]] +[[get-all-connectors-api-path-params]] ==== Path parameters `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-get-all-codes]] +[[get-all-connectors-api-codes]] ==== Response code `200`:: Indicates a successful call. -[[actions-and-connectors-api-get-all-example]] +[[get-all-connectors-api-example]] ==== Example [source,sh] diff --git a/docs/api/actions-and-connectors/legacy/create.asciidoc b/docs/api/actions-and-connectors/legacy/create.asciidoc index faf6227f01947..af4feddcb80fb 100644 --- a/docs/api/actions-and-connectors/legacy/create.asciidoc +++ b/docs/api/actions-and-connectors/legacy/create.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-create]] ==== Legacy Create connector API ++++ -Legacy Create connector API +Legacy Create connector ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Creates a connector. diff --git a/docs/api/actions-and-connectors/legacy/delete.asciidoc b/docs/api/actions-and-connectors/legacy/delete.asciidoc index b02f1011fd9b4..170fceba2d157 100644 --- a/docs/api/actions-and-connectors/legacy/delete.asciidoc +++ b/docs/api/actions-and-connectors/legacy/delete.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-delete]] ==== Legacy Delete connector API ++++ -Legacy Delete connector API +Legacy Delete connector ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Deletes a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/execute.asciidoc b/docs/api/actions-and-connectors/legacy/execute.asciidoc index 30cb18c54aa69..200844ab72f17 100644 --- a/docs/api/actions-and-connectors/legacy/execute.asciidoc +++ b/docs/api/actions-and-connectors/legacy/execute.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-execute]] ==== Legacy Execute connector API ++++ -Legacy Execute connector API +Legacy Execute connector ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Executes a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/get.asciidoc b/docs/api/actions-and-connectors/legacy/get.asciidoc index cf8cc1b6b677e..1b138fb7032e0 100644 --- a/docs/api/actions-and-connectors/legacy/get.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-get]] ==== Legacy Get connector API ++++ -Legacy Get connector API +Legacy Get connector ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Retrieves a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/get_all.asciidoc b/docs/api/actions-and-connectors/legacy/get_all.asciidoc index 24ad446d95d95..ba235955c005e 100644 --- a/docs/api/actions-and-connectors/legacy/get_all.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get_all.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-get-all]] ==== Legacy Get all connector API ++++ -Legacy Get all connector API +Legacy Get all connector ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Retrieves all connectors. diff --git a/docs/api/actions-and-connectors/legacy/list.asciidoc b/docs/api/actions-and-connectors/legacy/list.asciidoc index 86026f332d917..8acfd5415af57 100644 --- a/docs/api/actions-and-connectors/legacy/list.asciidoc +++ b/docs/api/actions-and-connectors/legacy/list.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-list]] ==== Legacy List connector types API ++++ -Legacy List all connector types API +Legacy List all connector types ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Retrieves a list of all connector types. diff --git a/docs/api/actions-and-connectors/legacy/update.asciidoc b/docs/api/actions-and-connectors/legacy/update.asciidoc index c2e841988717a..517daf9a40dca 100644 --- a/docs/api/actions-and-connectors/legacy/update.asciidoc +++ b/docs/api/actions-and-connectors/legacy/update.asciidoc @@ -1,12 +1,10 @@ [[actions-and-connectors-legacy-api-update]] ==== Legacy Update connector API ++++ -Legacy Update connector API +Legacy Update connector ++++ -deprecated::[7.13.0] - -Please use the <> instead. +WARNING: Deprecated in 7.13.0. Use <> instead. Updates the attributes for an existing connector. diff --git a/docs/api/actions-and-connectors/list.asciidoc b/docs/api/actions-and-connectors/list.asciidoc index 941f7b4376e91..bd1ccb777b9ae 100644 --- a/docs/api/actions-and-connectors/list.asciidoc +++ b/docs/api/actions-and-connectors/list.asciidoc @@ -1,31 +1,31 @@ -[[actions-and-connectors-api-list]] +[[list-connector-types-api]] === List connector types API ++++ -List all connector types API +List all connector types ++++ Retrieves a list of all connector types. -[[actions-and-connectors-api-list-request]] +[[list-connector-types-api-request]] ==== Request `GET :/api/actions/connector_types` `GET :/s//api/actions/connector_types` -[[actions-and-connectors-api-list-path-params]] +[[list-connector-types-api-path-params]] ==== Path parameters `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-list-codes]] +[[list-connector-types-api-codes]] ==== Response code `200`:: Indicates a successful call. -[[actions-and-connectors-api-list-example]] +[[list-connector-types-api-example]] ==== Example [source,sh] diff --git a/docs/api/actions-and-connectors/update.asciidoc b/docs/api/actions-and-connectors/update.asciidoc index 6c4e6040bdfb5..f522cb8d048e0 100644 --- a/docs/api/actions-and-connectors/update.asciidoc +++ b/docs/api/actions-and-connectors/update.asciidoc @@ -1,19 +1,19 @@ -[[actions-and-connectors-api-update]] +[[update-connector-api]] === Update connector API ++++ -Update connector API +Update connector ++++ Updates the attributes for an existing connector. -[[actions-and-connectors-api-update-request]] +[[update-connector-api-request]] ==== Request `PUT :/api/actions/connector/` `PUT :/s//api/actions/connector/` -[[actions-and-connectors-api-update-params]] +[[update-connector-api-params]] ==== Path parameters `id`:: @@ -22,7 +22,7 @@ Updates the attributes for an existing connector. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[[actions-and-connectors-api-update-request-body]] +[[update-connector-api-request-body]] ==== Request body `name`:: @@ -34,13 +34,13 @@ Updates the attributes for an existing connector. `secrets`:: (Required, object) The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. For information about the secrets configuration properties, refer to <>. -[[actions-and-connectors-api-update-codes]] +[[update-connector-api-codes]] ==== Response code `200`:: Indicates a successful call. -[[actions-and-connectors-api-update-example]] +[[update-connector-api-example]] ==== Example [source,sh] diff --git a/docs/api/alerting.asciidoc b/docs/api/alerting.asciidoc new file mode 100644 index 0000000000000..ad2d358d17ba0 --- /dev/null +++ b/docs/api/alerting.asciidoc @@ -0,0 +1,47 @@ +[[alerting-apis]] +== Alerting APIs + +The following APIs are available for {kib} alerting. + +* <> to create a rule + +* <> to update the attributes for existing rules + +* <> to retrieve a single rule by ID + +* <> to permanently remove a rule + +* <> to retrieve a paginated set of rules by condition + +* <> to retrieve a list of rule types + +* <> to enable a single rule by ID + +* <> to disable a single rule by ID + +* <> to mute alert for a single rule by ID + +* <> to unmute alert for a single rule by ID + +* <> to mute all alerts for a single rule by ID + +* <> to unmute all alerts for a single rule by ID + +* <> to retrieve the health of the Alerting framework + +For deprecated APIs, refer to <>. + +include::alerting/create_rule.asciidoc[] +include::alerting/update_rule.asciidoc[] +include::alerting/get_rules.asciidoc[] +include::alerting/delete_rule.asciidoc[] +include::alerting/find_rules.asciidoc[] +include::alerting/list_rule_types.asciidoc[] +include::alerting/enable_rule.asciidoc[] +include::alerting/disable_rule.asciidoc[] +include::alerting/mute_all_alerts.asciidoc[] +include::alerting/mute_alert.asciidoc[] +include::alerting/unmute_all_alerts.asciidoc[] +include::alerting/unmute_alert.asciidoc[] +include::alerting/health.asciidoc[] +include::alerting/legacy/index.asciidoc[] diff --git a/docs/api/alerting/create_rule.asciidoc b/docs/api/alerting/create_rule.asciidoc new file mode 100644 index 0000000000000..01b6dfc40fcf6 --- /dev/null +++ b/docs/api/alerting/create_rule.asciidoc @@ -0,0 +1,196 @@ +[[create-rule-api]] +=== Create rule API +++++ +Create rule +++++ + +Create {kib} rules. + +[[create-rule-api-request]] +==== Request + +`POST :/api/alerting/rule/` + +`POST :/s//api/alerting/rule/` + +[[create-rule-api-path-params]] +==== Path parameters + +``:: + (Optional, string) Specifies a UUID v1 or v4 to use instead of a randomly generated ID. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[create-rule-api-request-body]] +==== Request body + +`name`:: + (Required, string) A name to reference and search. + +`tags`:: + (Optional, string array) A list of keywords to reference and search. + +`rule_type_id`:: + (Required, string) The ID of the rule type that you want to call when the rule is scheduled to run. + +`schedule`:: + (Required, object) The schedule specifying when this rule should be run, using one of the available schedule formats specified under ++ +._Schedule Formats_. +[%collapsible%open] +===== +A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + +We currently support the _Interval format_ which specifies the interval in seconds, minutes, hours or days at which the rule should execute. +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +There are plans to support multiple other schedule formats in the near future. +===== + +`throttle`:: + (Optional, string) How often this rule should fire the same actions. This will prevent the rule from sending out the same notification over and over. For example, if a rule with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. + +`notify_when`:: + (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. + +`enabled`:: + (Optional, boolean) Indicates if you want to run the rule on an interval basis after it is created. + +`consumer`:: + (Required, string) The name of the application that owns the rule. This name has to match the Kibana Feature name, as that dictates the required RBAC privileges. + +`params`:: + (Required, object) The parameters to pass to the rule type executor `params` value. This will also validate against the rule type params validator, if defined. + +`actions`:: + (Optional, object array) An array of the following action objects. ++ +.Properties of the action objects: +[%collapsible%open] +===== + `group`::: + (Required, string) Grouping actions is recommended for escalations for different types of alerts. If you don't need this, set this value to `default`. + + `id`::: + (Required, string) The ID of the connector saved object to execute. + + `params`::: + (Required, object) The map to the `params` that the <> will receive. ` params` are handled as Mustache templates and passed a default set of context. +===== + + +[[create-rule-api-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[create-rule-api-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "params":{ + "aggType":"avg", + "termSize":6, + "thresholdComparator":">", + "timeWindowSize":5, + "timeWindowUnit":"m", + "groupBy":"top", + "threshold":[ + 1000 + ], + "index":[ + ".test-index" + ], + "timeField":"@timestamp", + "aggField":"sheet.version", + "termField":"name.keyword" + }, + "consumer":"alerts", + "rule_type_id":".index-threshold", + "schedule":{ + "interval":"1m" + }, + "actions":[ + { + "id":"dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2", + "group":"threshold met", + "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}}" + } + } + ], + "tags":[ + "cpu" + ], + "notify_when":"onActionGroupChange", + "name":"my alert" +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "41893910-6bca-11eb-9e0d-85d233e3ee35", + "notify_when": "onActionGroupChange", + "params": { + "aggType": "avg", + "termSize": 6, + "thresholdComparator": ">", + "timeWindowSize": 5, + "timeWindowUnit": "m", + "groupBy": "top", + "threshold": [ + 1000 + ], + "index": [ + ".kibana" + ], + "timeField": "@timestamp", + "aggField": "sheet.version", + "termField": "name.keyword" + }, + "consumer": "alerts", + "rule_type_id": ".index-threshold", + "schedule": { + "interval": "1m" + }, + "actions": [ + { + "connector_type_id": ".server-log", + "group": "threshold met", + "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}}" + }, + "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + ], + "tags": [ + "cpu" + ], + "name": "my alert", + "enabled": true, + "throttle": null, + "api_key_owner": "elastic", + "created_by": "elastic", + "updated_by": "elastic", + "mute_all": false, + "muted_alert_ids": [], + "updated_at": "2021-02-10T18:03:19.961Z", + "created_at": "2021-02-10T18:03:19.961Z", + "scheduled_task_id": "425b0800-6bca-11eb-9e0d-85d233e3ee35", + "execution_status": { + "last_execution_date": "2021-02-10T18:03:19.966Z", + "status": "pending" + } +} +-------------------------------------------------- diff --git a/docs/api/alerting/delete_rule.asciidoc b/docs/api/alerting/delete_rule.asciidoc new file mode 100644 index 0000000000000..29e642e04c9e2 --- /dev/null +++ b/docs/api/alerting/delete_rule.asciidoc @@ -0,0 +1,41 @@ +[[delete-rule-api]] +=== Delete rule API +++++ +Delete rule +++++ + +Permanently remove a rule. + +WARNING: Once you delete a rule, you cannot recover it. + +[[delete-rule-api-request]] +==== Request + +`DELETE :/api/alerting/rule/` + +`DELETE :/s//api/alerting/rule/` + +[[delete-rule-api-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule that you want to remove. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[delete-rule-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Delete a rule with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35 +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/disable_rule.asciidoc b/docs/api/alerting/disable_rule.asciidoc new file mode 100644 index 0000000000000..ce003335623ef --- /dev/null +++ b/docs/api/alerting/disable_rule.asciidoc @@ -0,0 +1,39 @@ +[[disable-rule-api]] +=== Disable rule API +++++ +Disable rule +++++ + +Disable a rule. + +[[disable-rule-api-request]] +==== Request + +`POST :/api/alerting/rule//_disable` + +`POST :/s//api/alerting/rule//_disable` + +[[disable-rule-api-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule that you want to disable. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[disable-rule-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Disable a rule with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35/_disable +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/enable_rule.asciidoc b/docs/api/alerting/enable_rule.asciidoc new file mode 100644 index 0000000000000..60f18b3510904 --- /dev/null +++ b/docs/api/alerting/enable_rule.asciidoc @@ -0,0 +1,39 @@ +[[enable-rule-api]] +=== Enable rule API +++++ +Enable rule +++++ + +Enable a rule. + +[[enable-rule-api-request]] +==== Request + +`POST :/api/alerting/rule//_enable` + +`POST :/s//api/alerting/rule//_enable` + +[[enable-rule-api-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule that you want to enable. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[enable-rule-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Enable a rule with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35/_enable +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/find_rules.asciidoc b/docs/api/alerting/find_rules.asciidoc new file mode 100644 index 0000000000000..2df8b3522725c --- /dev/null +++ b/docs/api/alerting/find_rules.asciidoc @@ -0,0 +1,127 @@ +[[find-rules-api]] +=== Find rules API +++++ +Find rules +++++ + +Retrieve a paginated set of rules based on condition. + +NOTE: As rules change in {kib}, the results on each page of the response also +change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + +[[find-rules-api-request]] +==== Request + +`GET :/api/alerting/rules/_find` + +`GET :/s//api/alerting/rules/_find` + +[[find-rules-api-path-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[find-rules-api-query-params]] +==== Query Parameters + +NOTE: Rule `params` are stored as a {ref}/flattened.html[flattened field type] and analyzed as keywords. + +`per_page`:: + (Optional, number) The number of rules to return per page. + +`page`:: + (Optional, number) The page number. + +`search`:: + (Optional, string) An Elasticsearch {ref}/query-dsl-simple-query-string-query.html[simple_query_string] query that filters the rules in the response. + +`default_search_operator`:: + (Optional, string) The operator to use for the `simple_query_string`. The default is 'OR'. + +`search_fields`:: + (Optional, array|string) The fields to perform the `simple_query_string` parsed query against. + +`fields`:: + (Optional, array|string) The fields to return in the `attributes` key of the response. + +`sort_field`:: + (Optional, string) Sorts the response. Could be a rule field returned in the `attributes` key of the response. + +`sort_order`:: + (Optional, string) Sort direction, either `asc` or `desc`. + +`has_reference`:: + (Optional, object) Filters the rules that have a relation with the reference objects with the specific "type" and "ID". + +`filter`:: + (Optional, string) A <> string that you filter with an attribute from your saved object. + It should look like savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object, such as `updatedAt`, + you will have to define your filter, for example, savedObjectType.updatedAt > 2018-12-22. + +[[find-rules-api-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Examples + +Find rules with names that start with `my`: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerting/rules/_find?search_fields=name&search=my* +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "page": 1, + "per_page": 10, + "total": 1, + "data": [ + { + "id": "0a037d60-6b62-11eb-9e0d-85d233e3ee35", + "notify_when": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "rule_type_id": "test.rule.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "test rule", + "enabled": true, + "throttle": null, + "api_key_owner": "elastic", + "created_by": "elastic", + "updated_by": "elastic", + "mute_all": false, + "muted_alert_ids": [], + "updated_at": "2021-02-10T05:37:19.086Z", + "created_at": "2021-02-10T05:37:19.086Z", + "scheduled_task_id": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "execution_status": { + "last_execution_date": "2021-02-10T17:55:14.262Z", + "status": "ok" + } + }, + ] +} +-------------------------------------------------- + +For parameters that accept multiple values (e.g. `fields`), repeat the +query parameter for each value: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerting/rules/_find?fields=id&fields=name +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/get_rules.asciidoc b/docs/api/alerting/get_rules.asciidoc new file mode 100644 index 0000000000000..1594ec1fb7ae6 --- /dev/null +++ b/docs/api/alerting/get_rules.asciidoc @@ -0,0 +1,75 @@ +[[get-rule-api]] +=== Get rule API +++++ +Get rule +++++ + +Retrieve a rule by ID. + +[[get-rule-api-request]] +==== Request + +`GET :/api/alerting/rule/` + +`GET :/s//api/alerting/rule/` + +[[get-rule-api-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule to retrieve. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[get-rule-api-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[get-rule-api-example]] +==== Example + +Retrieve the rule object with the ID `41893910-6bca-11eb-9e0d-85d233e3ee35`: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35 +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "0a037d60-6b62-11eb-9e0d-85d233e3ee35", + "notify_when": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "rule_type_id": "test.rule.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "test rule", + "enabled": true, + "throttle": null, + "api_key_owner": "elastic", + "created_by": "elastic", + "updated_by": "elastic", + "mute_all": false, + "muted_alert_ids": [], + "updated_at": "2021-02-10T05:37:19.086Z", + "created_at": "2021-02-10T05:37:19.086Z", + "scheduled_task_id": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "execution_status": { + "last_execution_date": "2021-02-10T17:55:14.262Z", + "status": "ok" + } +} +-------------------------------------------------- diff --git a/docs/api/alerting/health.asciidoc b/docs/api/alerting/health.asciidoc new file mode 100644 index 0000000000000..1e6b9ce22a981 --- /dev/null +++ b/docs/api/alerting/health.asciidoc @@ -0,0 +1,93 @@ +[[get-alerting-framework-health-api]] +=== Get Alerting framework health API +++++ +Get Alerting framework health +++++ + +Retrieve the health status of the Alerting framework. + +[[get-alerting-framework-health-api-request]] +==== Request + +`GET :/api/alerting/_health` + +`GET :/s//api/alerting/_health` + +[[get-alerting-framework-health-api-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[get-alerting-framework-health-api-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[get-alerting-framework-health-api-example]] +==== Example + +Retrieve the health status of the Alerting framework: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerting/_health +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "is_sufficiently_secure":true, + "has_permanent_encryption_key":true, + "alerting_framework_health":{ + "decryption_health":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + }, + "execution_health":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + }, + "read_health":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + } + } +} +-------------------------------------------------- + +The health API response contains the following properties: + +[cols="2*<"] +|=== + +| `is_sufficiently_secure` +| Returns `false` if security is enabled, but TLS is not. + +| `has_permanent_encryption_key` +| Return the state `false` if Encrypted Saved Object plugin has not a permanent encryption Key. + +| `alerting_framework_health` +| This state property has three substates that identify the health of the alerting framework API: `decryption_health`, `execution_health`, and `read_health`. + +|=== + +`alerting_framework_health` consists of the following properties: + +[cols="2*<"] +|=== + +| `decryption_health` +| Returns the timestamp and status of the rule decryption: `ok`, `warn` or `error` . + +| `execution_health` +| Returns the timestamp and status of the rule execution: `ok`, `warn` or `error`. + +| `read_health` +| Returns the timestamp and status of the rule reading events: `ok`, `warn` or `error`. + +|=== diff --git a/docs/api/alerts/create.asciidoc b/docs/api/alerting/legacy/create.asciidoc similarity index 97% rename from docs/api/alerts/create.asciidoc rename to docs/api/alerting/legacy/create.asciidoc index c3e6d36813972..5c594d64a3f45 100644 --- a/docs/api/alerts/create.asciidoc +++ b/docs/api/alerting/legacy/create.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-create]] -=== Create alert API +=== Legacy create alert API ++++ -Create alert +Legacy create alert ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Create {kib} alerts. [[alerts-api-create-request]] diff --git a/docs/api/alerts/delete.asciidoc b/docs/api/alerting/legacy/delete.asciidoc similarity index 86% rename from docs/api/alerts/delete.asciidoc rename to docs/api/alerting/legacy/delete.asciidoc index 72dfd5e87336c..68851973cab5b 100644 --- a/docs/api/alerts/delete.asciidoc +++ b/docs/api/alerting/legacy/delete.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-delete]] -=== Delete alert API +=== Legacy delete alert API ++++ -Delete alert +Legacy delete alert ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Permanently remove an alert. WARNING: Once you delete an alert, you cannot recover it. diff --git a/docs/api/alerts/disable.asciidoc b/docs/api/alerting/legacy/disable.asciidoc similarity index 85% rename from docs/api/alerts/disable.asciidoc rename to docs/api/alerting/legacy/disable.asciidoc index 86c58c37c2ecd..56e06371570c2 100644 --- a/docs/api/alerts/disable.asciidoc +++ b/docs/api/alerting/legacy/disable.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-disable]] -=== Disable alert API +=== Legacy disable alert API ++++ -Disable alert +Legacy disable alert ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Disable an alert. [[alerts-api-disable-request]] diff --git a/docs/api/alerts/enable.asciidoc b/docs/api/alerting/legacy/enable.asciidoc similarity index 85% rename from docs/api/alerts/enable.asciidoc rename to docs/api/alerting/legacy/enable.asciidoc index de1a5f7985a38..913d96a84352b 100644 --- a/docs/api/alerts/enable.asciidoc +++ b/docs/api/alerting/legacy/enable.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-enable]] -=== Enable alert API +=== Legacy enable alert API ++++ -Enable alert +Legacy enable alert ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Enable an alert. [[alerts-api-enable-request]] diff --git a/docs/api/alerts/find.asciidoc b/docs/api/alerting/legacy/find.asciidoc similarity index 96% rename from docs/api/alerts/find.asciidoc rename to docs/api/alerting/legacy/find.asciidoc index cc66d4e0f4183..94d9bc425bd21 100644 --- a/docs/api/alerts/find.asciidoc +++ b/docs/api/alerting/legacy/find.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-find]] -=== Find alerts API +=== Legacy find alerts API ++++ -Find alerts +Legacy find alerts ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Retrieve a paginated set of alerts based on condition. NOTE: As alerts change in {kib}, the results on each page of the response also diff --git a/docs/api/alerts/get.asciidoc b/docs/api/alerting/legacy/get.asciidoc similarity index 92% rename from docs/api/alerts/get.asciidoc rename to docs/api/alerting/legacy/get.asciidoc index 433605e857332..f1014d18e8774 100644 --- a/docs/api/alerts/get.asciidoc +++ b/docs/api/alerting/legacy/get.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-get]] -=== Get alert API +=== Legacy get alert API ++++ -Get alert +Legacy get alert ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Retrieve an alert by ID. [[alerts-api-get-request]] diff --git a/docs/api/alerts/health.asciidoc b/docs/api/alerting/legacy/health.asciidoc similarity index 92% rename from docs/api/alerts/health.asciidoc rename to docs/api/alerting/legacy/health.asciidoc index b29e5def53384..b25307fb5efd1 100644 --- a/docs/api/alerts/health.asciidoc +++ b/docs/api/alerting/legacy/health.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-health]] -=== Get Alerting framework health API +=== Legacy get Alerting framework health API ++++ -Get Alerting framework health +Legacy get Alerting framework health ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Retrieve the health status of the Alerting framework. [[alerts-api-health-request]] diff --git a/docs/api/alerting/legacy/index.asciidoc b/docs/api/alerting/legacy/index.asciidoc new file mode 100644 index 0000000000000..cce2c378bdb58 --- /dev/null +++ b/docs/api/alerting/legacy/index.asciidoc @@ -0,0 +1,18 @@ +[[alerts-api]] +=== Deprecated 7.x APIs + +These APIs are deprecated and will be removed as of 8.0. + +include::create.asciidoc[leveloffset=+1] +include::delete.asciidoc[leveloffset=+1] +include::disable.asciidoc[leveloffset=+1] +include::enable.asciidoc[leveloffset=+1] +include::find.asciidoc[leveloffset=+1] +include::get.asciidoc[leveloffset=+1] +include::health.asciidoc[leveloffset=+1] +include::list.asciidoc[leveloffset=+1] +include::mute.asciidoc[leveloffset=+1] +include::mute_all.asciidoc[leveloffset=+1] +include::unmute.asciidoc[leveloffset=+1] +include::unmute_all.asciidoc[leveloffset=+1] +include::update.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/api/alerts/list.asciidoc b/docs/api/alerting/legacy/list.asciidoc similarity index 96% rename from docs/api/alerts/list.asciidoc rename to docs/api/alerting/legacy/list.asciidoc index e180945accfd3..e9ef3bbc27cd9 100644 --- a/docs/api/alerts/list.asciidoc +++ b/docs/api/alerting/legacy/list.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-list]] -=== List alert types API +=== Legacy list alert types API ++++ -List all alert types API +Legacy list all alert types ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Retrieve a list of all alert types. [[alerts-api-list-request]] diff --git a/docs/api/alerts/mute.asciidoc b/docs/api/alerting/legacy/mute.asciidoc similarity index 87% rename from docs/api/alerts/mute.asciidoc rename to docs/api/alerting/legacy/mute.asciidoc index 84a2996b65838..dff42f5911e53 100644 --- a/docs/api/alerts/mute.asciidoc +++ b/docs/api/alerting/legacy/mute.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-mute]] -=== Mute alert instance API +=== Legacy mute alert instance API ++++ -Mute alert instance +Legacy mute alert instance ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Mute an alert instance. [[alerts-api-mute-request]] diff --git a/docs/api/alerts/mute_all.asciidoc b/docs/api/alerting/legacy/mute_all.asciidoc similarity index 83% rename from docs/api/alerts/mute_all.asciidoc rename to docs/api/alerting/legacy/mute_all.asciidoc index 02f41eb3b768e..df89fa15d1590 100644 --- a/docs/api/alerts/mute_all.asciidoc +++ b/docs/api/alerting/legacy/mute_all.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-mute-all]] -=== Mute all alert instances API +=== Legacy mute all alert instances API ++++ -Mute all alert instances +Legacy mute all alert instances ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Mute all alert instances. [[alerts-api-mute-all-request]] diff --git a/docs/api/alerts/unmute.asciidoc b/docs/api/alerting/legacy/unmute.asciidoc similarity index 87% rename from docs/api/alerts/unmute.asciidoc rename to docs/api/alerting/legacy/unmute.asciidoc index eb73bb539154f..0be7e40dc1a19 100644 --- a/docs/api/alerts/unmute.asciidoc +++ b/docs/api/alerting/legacy/unmute.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-unmute]] -=== Unmute alert instance API +=== Legacy unmute alert instance API ++++ -Unmute alert instance +Legacy unmute alert instance ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Unmute an alert instance. [[alerts-api-unmute-request]] diff --git a/docs/api/alerts/unmute_all.asciidoc b/docs/api/alerting/legacy/unmute_all.asciidoc similarity index 83% rename from docs/api/alerts/unmute_all.asciidoc rename to docs/api/alerting/legacy/unmute_all.asciidoc index a20a20fd8204a..8687c2d2fe8bb 100644 --- a/docs/api/alerts/unmute_all.asciidoc +++ b/docs/api/alerting/legacy/unmute_all.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-unmute-all]] -=== Unmute all alert instances API +=== Legacy unmute all alert instances API ++++ -Unmute all alert instances +Legacy unmute all alert instances ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Unmute all alert instances. [[alerts-api-unmute-all-request]] diff --git a/docs/api/alerts/update.asciidoc b/docs/api/alerting/legacy/update.asciidoc similarity index 96% rename from docs/api/alerts/update.asciidoc rename to docs/api/alerting/legacy/update.asciidoc index a0b147ed4a15d..bffdf26c31400 100644 --- a/docs/api/alerts/update.asciidoc +++ b/docs/api/alerting/legacy/update.asciidoc @@ -1,9 +1,11 @@ [[alerts-api-update]] -=== Update alert API +=== Legacy update alert API ++++ -Update alert +Legacy update alert ++++ +WARNING: Deprecated in 7.13.0. Use <> instead. + Update the attributes for an existing alert. [[alerts-api-update-request]] diff --git a/docs/api/alerting/list_rule_types.asciidoc b/docs/api/alerting/list_rule_types.asciidoc new file mode 100644 index 0000000000000..77ca8601a6e8b --- /dev/null +++ b/docs/api/alerting/list_rule_types.asciidoc @@ -0,0 +1,135 @@ +[[list-rule-types-api]] +=== List rule types API +++++ +List rule types +++++ + +Retrieve a list of alerting rule types. + +[[list-rule-types-api-request]] +==== Request + +`GET :/api/alerting/rule_types` + +`GET :/s//api/alerting/rule_types` + +[[list-rule-types-api-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[list-rule-types-api-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[list-rule-types-api-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerting/rule_types +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id":".index-threshold", + "name":"Index threshold", + "action_groups":[ + { + "id":"threshold met", + "name":"Threshold met" + }, + { + "id":"recovered", + "name":"Recovered" + } + ], + "recovery_action_group":{ + "id":"recovered", + "name":"Recovered" + }, + "default_action_group_id":"threshold met", + "action_variables":{ + "context":[ + { + "name":"message", + "description":"A pre-constructed message for the alert." + }, + ], + "state":[], + "params":[ + { + "name":"threshold", + "description":"An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one." + }, + { + "name":"index", + "description":"index" + }, + ] + }, + "producer":"stackAlerts", + "minimum_license_required":"basic", + "enabled_in_license":true, + "authorized_consumers":{ + "alerts":{ + "read":true, + "all":true + }, + "stackAlerts":{ + "read":true, + "all":true + }, + "uptime":{ + "read":true, + "all":true + } + } + } +] +-------------------------------------------------- + +Each rule type contains the following properties: + +[cols="2*<"] +|=== + +| `name` +| The descriptive name of the rule type. + +| `id` +| The unique ID of the rule type. + +| `minimum_license_required` +| The license required to use the rule type. + +| `enabled_in_license` +| Whether the rule type is enabled or disabled based on the license. + +| `action_groups` +| An explicit list of groups for which the rule type can schedule actions, each with the action group's unique ID and human readable name. Rule `actions` validation will use this configuration to ensure that groups are valid. Use `kbn-i18n` to translate the names of the action group when registering the rule type. + +| `recovery_action_group` +| An action group to use when an alert goes from an active state, to an inactive one. Do not specify this action group under the `action_groups` property. If `recovery_action_group` is not specified, the default `recovered` action group is used. + +| `default_action_group_od` +| The default ID for the rule type group. + +| `action_variables` +| An explicit list of action variables that the rule type makes available via context and state in action parameter templates, and a short human readable description. The Rule UI will use this information to prompt users for these variables in action parameter editors. Use `kbn-i18n` to translate the descriptions. + +| `producer` +| The ID of the application producing this rule type. + +| `authorized_consumers` +| The list of the plugins IDs that have access to the rule type. + +|=== diff --git a/docs/api/alerting/mute_alert.asciidoc b/docs/api/alerting/mute_alert.asciidoc new file mode 100644 index 0000000000000..4ebf12d1ce10c --- /dev/null +++ b/docs/api/alerting/mute_alert.asciidoc @@ -0,0 +1,42 @@ +[[mute-alert-api]] +=== Mute alert API +++++ +Mute alert +++++ + +Mute an alert. + +[[mute-alert-api-request]] +==== Request + +`POST :/api/alerting/rule//alert//_mute` + +`POST :/s//api/alerting/rule//alert//_mute` + +[[mute-alert-api-path-params]] +==== Path parameters + +`rule_id`:: + (Required, string) The ID of the rule whose alert you want to mute. + +`alert_id`:: + (Required, string) The ID of the alert that you want to mute. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[mute-alert-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Mute alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35/alert/dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2/_mute +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/mute_all_alerts.asciidoc b/docs/api/alerting/mute_all_alerts.asciidoc new file mode 100644 index 0000000000000..58b6b14f49b4f --- /dev/null +++ b/docs/api/alerting/mute_all_alerts.asciidoc @@ -0,0 +1,39 @@ +[[mute-all-alerts-api]] +=== Mute all alerts API +++++ +Mute all alerts +++++ + +Mute all alerts. + +[[mute-all-alerts-api-request]] +==== Request + +`POST :/api/alerting/rule//_mute_all` + +`POST :/s//api/alerting/rule//_mute_all` + +[[mute-all-alerts-api-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule whose alerts you want to mute. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[mute-all-alerts-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Mute all alerts with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35/_mute_all +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/unmute_alert.asciidoc b/docs/api/alerting/unmute_alert.asciidoc new file mode 100644 index 0000000000000..6e8870bb2fdae --- /dev/null +++ b/docs/api/alerting/unmute_alert.asciidoc @@ -0,0 +1,42 @@ +[[unmute-alert-api]] +=== Unmute alert API +++++ +Unmute alert +++++ + +Unmute an alert. + +[[unmute-alert-api-request]] +==== Request + +`POST :/api/alerting/rule//alert//_unmute` + +`POST :/s//api/alerting/rule//alert//_unmute` + +[[unmute-alert-api-path-params]] +==== Path parameters + +`rule_id`:: + (Required, string) The ID of the rule whose alert you want to mute. + +`alert_id`:: + (Required, string) The ID of the alert that you want to unmute. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[unmute-alert-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Unmute alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35/alert/dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2/_unmute +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/unmute_all_alerts.asciidoc b/docs/api/alerting/unmute_all_alerts.asciidoc new file mode 100644 index 0000000000000..c429ca288ae79 --- /dev/null +++ b/docs/api/alerting/unmute_all_alerts.asciidoc @@ -0,0 +1,39 @@ +[[unmute-all-alerts-api]] +=== Unmute all alerts API +++++ +Unmute all alerts +++++ + +Unmute all alerts. + +[[unmute-all-alerts-api-all-request]] +==== Request + +`POST :/api/alerting/rule//_unmute_all` + +`POST :/s//api/alerting/rule//_unmute_all` + +[[unmute-all-alerts-api-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule whose alerts you want to unmute. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[unmute-all-alerts-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Unmute all alerts with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35/_unmute_all +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerting/update_rule.asciidoc b/docs/api/alerting/update_rule.asciidoc new file mode 100644 index 0000000000000..76c88a009be01 --- /dev/null +++ b/docs/api/alerting/update_rule.asciidoc @@ -0,0 +1,136 @@ +[[update-rule-api]] +=== Update rule API +++++ +Update rule +++++ + +Update the attributes for an existing rule. + +[[update-rule-api-request]] +==== Request + +`PUT :/api/alerting/rule/` + +`PUT :/s//api/alerting/rule/` + +[[update-rule-api-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the rule that you want to update. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[update-rule-api-request-body]] +==== Request body + +`name`:: + (Required, string) A name to reference and search. + +`tags`:: + (Optional, string array) A list of keywords to reference and search. + +`schedule`:: + (Required, object) When to run this rule. Use one of the available schedule formats. ++ +._Schedule Formats_. +[%collapsible%open] +===== +A schedule uses a key: value format. {kib} currently supports the _Interval format_ , which specifies the interval in seconds, minutes, hours, or days at which to execute the rule. + +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +===== + +`throttle`:: + (Optional, string) How often this rule should fire the same actions. This will prevent the rule from sending out the same notification over and over. For example, if a rule with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. + +`notify_when`:: + (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. + +`params`:: + (Required, object) The parameters to pass to the rule type executor `params` value. This will also validate against the rule type params validator, if defined. + +`actions`:: + (Optional, object array) An array of the following action objects. ++ +.Properties of the action objects: +[%collapsible%open] +===== + `group`::: + (Required, string) Grouping actions is recommended for escalations for different types of alerts. If you don't need this, set the value to `default`. + + `id`::: + (Required, string) The ID of the action that saved object executes. + + `params`::: + (Required, object) The map to the `params` that the <> will receive. `params` are handled as Mustache templates and passed a default set of context. +===== + + +[[update-rule-api-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[update-rule-api-example]] +==== Example + +Update a rule with ID `ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74` with a different name: + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/alerting/rule/ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74 + +{ + "notify_when": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "new name", + "throttle": null, +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74", + "notify_when": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "rule_type_id": "test.rule.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "new name", + "enabled": true, + "throttle": null, + "api_key_owner": "elastic", + "created_by": "elastic", + "updated_by": "elastic", + "mute_all": false, + "muted_alert_ids": [], + "updated_at": "2021-02-10T05:37:19.086Z", + "created_at": "2021-02-10T05:37:19.086Z", + "scheduled_task_id": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "execution_status": { + "last_execution_date": "2021-02-10T17:55:14.262Z", + "status": "ok" + } +} +-------------------------------------------------- diff --git a/docs/api/alerts.asciidoc b/docs/api/alerts.asciidoc deleted file mode 100644 index a19c538bcb4d7..0000000000000 --- a/docs/api/alerts.asciidoc +++ /dev/null @@ -1,42 +0,0 @@ -[[alerts-api]] -== Alerts APIs - -The following APIs are available for managing {kib} alerts. - -* <> to create an alert - -* <> to update the attributes for existing alerts - -* <> to retrieve a single alert by ID - -* <> to permanently remove an alert - -* <> to retrieve a paginated set of alerts by condition - -* <> to retrieve a list of all alert types - -* <> to enable a single alert by ID - -* <> to disable a single alert by ID - -* <> to mute alert instances for a single alert by ID - -* <> to unmute alert instances for a single alert by ID - -* <> to unmute all alert instances for a single alert by ID - -* <> to retrieve the health of the alerts framework - -include::alerts/create.asciidoc[] -include::alerts/update.asciidoc[] -include::alerts/get.asciidoc[] -include::alerts/delete.asciidoc[] -include::alerts/find.asciidoc[] -include::alerts/list.asciidoc[] -include::alerts/enable.asciidoc[] -include::alerts/disable.asciidoc[] -include::alerts/mute_all.asciidoc[] -include::alerts/mute.asciidoc[] -include::alerts/unmute_all.asciidoc[] -include::alerts/unmute.asciidoc[] -include::alerts/health.asciidoc[] diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index c185fdb43faf1..e448c0beb8b99 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -6,6 +6,24 @@ Get started ++++ +// Conditionally display a screenshot or video depending on what the +// current documentation version is. + +ifeval::["{is-current-version}"=="true"] +++++ + + +
+++++ +endif::[] + For a quick, high-level overview of the health and performance of your application, start with: diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index a3ac62a4c8343..99a6205ae010e 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -8,14 +8,34 @@ requests per minute, and errors per minute. If enabled, service maps also integrate with machine learning--for real time health indicators based on anomaly detection scores. All of these features can help you quickly and visually assess your services' status and health. +// Conditionally display a screenshot or video depending on what the +// current documentation version is. + +ifeval::["{is-current-version}"=="true"] +++++ + + +
+++++ +endif::[] + +ifeval::["{is-current-version}"=="false"] +[role="screenshot"] +image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana] +endif::[] + We currently surface two types of service maps: * Global: All services instrumented with APM agents and the connections between them are shown. * Service-specific: Highlight connections for a selected service. -[role="screenshot"] -image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana] - [float] [[service-maps-how]] === How do service maps work? diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 36d021d64456e..693046d652943 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -38,6 +38,8 @@ image::apm/images/traffic-transactions.png[Traffic and transactions] === Error rate and errors The *Error rate* chart displays the average error rates relating to the service, within a specific time range. +An HTTP response code greater than 400 does not necessarily indicate a failed transaction. +<>. The *Errors* table provides a high-level view of each error message when it first and last occurred, along with the total number of occurrences. This makes it very easy to quickly see which errors affect diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 8c8da81aa577e..c2a3e0bc2502d 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -22,11 +22,21 @@ Visualize response codes: `2xx`, `3xx`, `4xx`, etc. Useful for determining if more responses than usual are being served with a particular response code. Like in the latency graph, you can zoom in on anomalies to further investigate them. +[[transaction-error-rate]] *Error rate*:: -Visualize the total number of transactions with errors divided by the total number of transactions. -The error rate value is based on the `event.outcome` field and is the relative number of failed transactions. -Any unexpected increases, decreases, or irregular patterns can be investigated further -with the <>. +The error rate represents the percentage of failed transactions from the perspective of the selected service. +It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. ++ +[TIP] +==== +HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure +because the failure was caused by the caller, not the HTTP server. Thus, there will be no increase in error rate. + +HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. +These spans will increase the error rate. + +If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. +==== *Average duration by span type*:: Visualize where your application is spending most of its time. diff --git a/docs/developer/architecture/core/application_service.asciidoc b/docs/developer/architecture/core/application_service.asciidoc new file mode 100644 index 0000000000000..ba3c6bbed72be --- /dev/null +++ b/docs/developer/architecture/core/application_service.asciidoc @@ -0,0 +1,40 @@ +[[application-service]] +== Application service +Kibana has migrated to be a Single Page Application. Plugins should use `Application service` API to instruct Kibana that an application should be loaded and rendered in the UI in response to user interactions. The service also provides utilities for controlling the navigation link state, seamlessly integrating routing between applications, and loading async chunks on demand. + +NOTE: The Application service is only available client side. + +[source,typescript] +---- +import { AppMountParameters, CoreSetup, Plugin, DEFAULT_APP_CATEGORIES } from 'kibana/public'; + +export class MyPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ // <1> + category: DEFAULT_APP_CATEGORIES.kibana, + id: 'my-plugin', + title: 'my plugin title', + euiIconType: '/path/to/some.svg', + order: 100, + appRoute: '/app/my_plugin', // <2> + async mount(params: AppMountParameters) { // <3> + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services + const [coreStart, depsStart] = await core.getStartServices(); // <4> + // Render the application + return renderApp(coreStart, depsStart, params); // <5> + }, + }); + } +} +---- +<1> See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md[application.register interface] +<2> Application specific URL. +<3> `mount` callback is invoked when a user navigates to the application-specific URL. +<4> `core.getStartServices` method provides API available during `start` lifecycle. +<5> `mount` method must return a function that will be called to unmount the application, which is called when Kibana unmounts the application. You can put a clean-up logic there. + +NOTE: you are free to use any UI library to render a plugin application in DOM. +However, we recommend using React and https://elastic.github.io/eui[EUI] for all your basic UI +components to create a consistent UI experience. diff --git a/docs/developer/architecture/core/configuration-service.asciidoc b/docs/developer/architecture/core/configuration-service.asciidoc new file mode 100644 index 0000000000000..031135c7b790f --- /dev/null +++ b/docs/developer/architecture/core/configuration-service.asciidoc @@ -0,0 +1,149 @@ +[[configuration-service]] +== Configuration service +{kib} provides `ConfigService` for plugin developers that want to support +adjustable runtime behavior for their plugins. +Plugins can only read their own configuration values, it is not possible to access the configuration values from {kib} Core or other plugins directly. + +NOTE: The Configuration service is only available server side. + +[source,js] +---- +// in Legacy platform +const basePath = config.get('server.basePath'); +// in Kibana Platform 'basePath' belongs to the http service +const basePath = core.http.basePath.get(request); +---- + +To have access to your plugin config, you _should_: + +* Declare plugin-specific `configPath` (will fallback to plugin `id` +if not specified) in {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md[`kibana.json`] manifest file. +* Export schema validation for the config from plugin's main file. Schema is +mandatory. If a plugin reads from the config without schema declaration, +`ConfigService` will throw an error. + +*my_plugin/server/index.ts* +[source,typescript] +---- +import { schema, TypeOf } from '@kbn/config-schema'; +export const plugin = … +export const config = { + schema: schema.object(…), +}; +export type MyPluginConfigType = TypeOf; +---- + +* Read config value exposed via `PluginInitializerContext`: + +*my_plugin/server/index.ts* +[source,typescript] +---- +import type { PluginInitializerContext } from 'kibana/server'; +export class MyPlugin { + constructor(initializerContext: PluginInitializerContext) { + this.config$ = initializerContext.config.create(); + // or if config is optional: + this.config$ = initializerContext.config.createIfExists(); + } + ... +} +---- + +If your plugin also has a client-side part, you can also expose +configuration properties to it using the configuration `exposeToBrowser` +allow-list property. + +*my_plugin/server/index.ts* +[source,typescript] +---- +import { schema, TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Only on server' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + uiProp: true, + }, + schema: configSchema, +}; +---- + +Configuration containing only the exposed properties will be then +available on the client-side using the plugin's `initializerContext`: + +*my_plugin/public/index.ts* +[source,typescript] +---- +interface ClientConfigType { + uiProp: string; +} + +export class MyPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + const config = this.initializerContext.config.get(); + } +---- + +All plugins are considered enabled by default. If you want to disable +your plugin, you could declare the `enabled` flag in the plugin +config. This is a special {kib} Platform key. {kib} reads its +value and won’t create a plugin instance if `enabled: false`. + +[source,js] +---- +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; +---- +[[handle-plugin-configuration-deprecations]] +=== Handle plugin configuration deprecations +If your plugin has deprecated configuration keys, you can describe them using +the `deprecations` config descriptor field. +Deprecations are managed on a per-plugin basis, meaning you don’t need to specify +the whole property path, but use the relative path from your plugin’s +configuration root. + +*my_plugin/server/index.ts* +[source,typescript] +---- +import { schema, TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + newProperty: schema.string({ defaultValue: 'Some string' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('oldProperty', 'newProperty'), + unused('someUnusedProperty'), + ], +}; +---- + +In some cases, accessing the whole configuration for deprecations is +necessary. For these edge cases, `renameFromRoot` and `unusedFromRoot` +are also accessible when declaring deprecations. + +*my_plugin/server/index.ts* +[source,typescript] +---- +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot, unusedFromRoot }) => [ + renameFromRoot('oldplugin.property', 'myplugin.property'), + unusedFromRoot('oldplugin.deprecated'), + ], +}; +---- diff --git a/docs/developer/architecture/core/elasticsearch-service.asciidoc b/docs/developer/architecture/core/elasticsearch-service.asciidoc new file mode 100644 index 0000000000000..55632c0117938 --- /dev/null +++ b/docs/developer/architecture/core/elasticsearch-service.asciidoc @@ -0,0 +1,30 @@ +[[elasticsearch-service]] +== Elasticsearch service +`Elasticsearch service` provides `elasticsearch.client` program API to communicate with Elasticsearch server HTTP API. + +NOTE: The Elasticsearch service is only available server side. You can use the {kib-repo}blob/{branch}/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md[Data plugin] APIs on the client side. + +`elasticsearch.client` interacts with Elasticsearch service on behalf of: + +- `kibana_system` user via `elasticsearch.client.asInternalUser.*` methods. +- a current end-user via `elasticsearch.client.asCurrentUser.*` methods. In this case Elasticsearch client should be given the current user credentials. +See <> and <>. + +{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md[Elasticsearch service API docs] + +[source,typescript] +---- +import { CoreStart, Plugin } from 'kibana/public'; + +export class MyPlugin implements Plugin { + public start(core: CoreStart) { + async function asyncTask() { + const result = await core.elasticsearch.client.asInternalUser.ping(…); + } + asyncTask(); + } +} +---- + +For advanced use-cases, such as a search, use {kib-repo}blob/{branch}/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md[Data plugin] + diff --git a/docs/developer/architecture/core/http-service.asciidoc b/docs/developer/architecture/core/http-service.asciidoc new file mode 100644 index 0000000000000..45468d618dd09 --- /dev/null +++ b/docs/developer/architecture/core/http-service.asciidoc @@ -0,0 +1,67 @@ +[[http-service]] +== HTTP service + +NOTE: The HTTP service is available both server and client side. + +=== Server side usage + +The server-side HttpService allows server-side plugins to register endpoints with built-in support for request validation. These endpoints may be used by client-side code or be exposed as a public API for users. Most plugins integrate directly with this service. + +The service allows plugins to: +* to extend the {kib} server with custom HTTP API. +* to execute custom logic on an incoming request or server response. +* implement custom authentication and authorization strategy. + +See {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md[HTTP service API docs] + +[source,typescript] +---- +import { schema } from '@kbn/config-schema'; +import type { CoreSetup, Plugin } from 'kibana/server'; + +export class MyPlugin implements Plugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + + const validate = { + params: schema.object({ + id: schema.string(), + }), + }; + + router.get({ + path: 'my_plugin/{id}', + validate + }, + async (context, request, response) => { + const data = await findObject(request.params.id); + if (!data) return response.notFound(); + return response.ok({ + body: data, + headers: { + 'content-type': 'application/json' + } + }); + }); + } +} +---- + +=== Client side usage + +The HTTP service is also offered on the client side and provides an API to communicate with the {kib} server via HTTP interface. +The client-side HttpService is a preconfigured wrapper around `window.fetch` that includes some default behavior and automatically handles common errors (such as session expiration). The service should only be used for access to backend endpoints registered by the same plugin. Feel free to use another HTTP client library to request 3rd party services. + +[source,typescript] +---- +import { CoreStart } from 'kibana/public'; +interface ResponseType {…}; +interface MyPluginData {…}; +async function fetchData(core: CoreStart) { + return await core.http.get( + '/api/my_plugin/', + { query: … }, + ); +} +---- +See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.httpsetup.md[for all available API]. diff --git a/docs/developer/architecture/core/index.asciidoc b/docs/developer/architecture/core/index.asciidoc index 4a86c90cf8c10..53720a593d3f2 100644 --- a/docs/developer/architecture/core/index.asciidoc +++ b/docs/developer/architecture/core/index.asciidoc @@ -27,421 +27,18 @@ export class MyPlugin { } ---- -=== Server-side -[[configuration-service]] -==== Configuration service -{kib} provides `ConfigService` if a plugin developer may want to support -adjustable runtime behavior for their plugins. -Plugins can only read their own configuration values, it is not possible to access the configuration values from {kib} Core or other plugins directly. +The services that core provides are: -[source,js] ----- -// in Legacy platform -const basePath = config.get('server.basePath'); -// in Kibana Platform 'basePath' belongs to the http service -const basePath = core.http.basePath.get(request); ----- - -To have access to your plugin config, you _should_: +* <> +* <> +* <> +* <> +* <> +* <> +* <> -* Declare plugin-specific `configPath` (will fallback to plugin `id` -if not specified) in {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md[`kibana.json`] manifest file. -* Export schema validation for the config from plugin's main file. Schema is -mandatory. If a plugin reads from the config without schema declaration, -`ConfigService` will throw an error. - -*my_plugin/server/index.ts* -[source,typescript] ----- -import { schema, TypeOf } from '@kbn/config-schema'; -export const plugin = … -export const config = { - schema: schema.object(…), -}; -export type MyPluginConfigType = TypeOf; ----- - -* Read config value exposed via `PluginInitializerContext`. -*my_plugin/server/index.ts* -[source,typescript] ----- -import type { PluginInitializerContext } from 'kibana/server'; -export class MyPlugin { - constructor(initializerContext: PluginInitializerContext) { - this.config$ = initializerContext.config.create(); - // or if config is optional: - this.config$ = initializerContext.config.createIfExists(); - } ----- - -If your plugin also has a client-side part, you can also expose -configuration properties to it using the configuration `exposeToBrowser` -allow-list property. - -*my_plugin/server/index.ts* -[source,typescript] ----- -import { schema, TypeOf } from '@kbn/config-schema'; -import type { PluginConfigDescriptor } from 'kibana/server'; -const configSchema = schema.object({ - secret: schema.string({ defaultValue: 'Only on server' }), - uiProp: schema.string({ defaultValue: 'Accessible from client' }), -}); -type ConfigType = TypeOf; -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - uiProp: true, - }, - schema: configSchema, -}; ----- - -Configuration containing only the exposed properties will be then -available on the client-side using the plugin's `initializerContext`: - -*my_plugin/public/index.ts* -[source,typescript] ----- -interface ClientConfigType { - uiProp: string; -} - -export class MyPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup(core: CoreSetup, deps: {}) { - const config = this.initializerContext.config.get(); - } ----- - -All plugins are considered enabled by default. If you want to disable -your plugin, you could declare the `enabled` flag in the plugin -config. This is a special {kib} Platform key. {kib} reads its -value and won’t create a plugin instance if `enabled: false`. - -[source,js] ----- -export const config = { - schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), -}; ----- -[[handle-plugin-configuration-deprecations]] -===== Handle plugin configuration deprecations -If your plugin has deprecated configuration keys, you can describe them using -the `deprecations` config descriptor field. -Deprecations are managed on a per-plugin basis, meaning you don’t need to specify -the whole property path, but use the relative path from your plugin’s -configuration root. - -*my_plugin/server/index.ts* -[source,typescript] ----- -import { schema, TypeOf } from '@kbn/config-schema'; -import type { PluginConfigDescriptor } from 'kibana/server'; - -const configSchema = schema.object({ - newProperty: schema.string({ defaultValue: 'Some string' }), -}); - -type ConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: ({ rename, unused }) => [ - rename('oldProperty', 'newProperty'), - unused('someUnusedProperty'), - ], -}; ----- - -In some cases, accessing the whole configuration for deprecations is -necessary. For these edge cases, `renameFromRoot` and `unusedFromRoot` -are also accessible when declaring deprecations. - -*my_plugin/server/index.ts* -[source,typescript] ----- -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: ({ renameFromRoot, unusedFromRoot }) => [ - renameFromRoot('oldplugin.property', 'myplugin.property'), - unusedFromRoot('oldplugin.deprecated'), - ], -}; ----- -==== Logging service -Allows a plugin to provide status and diagnostic information. -For detailed instructions see the {kib-repo}blob/{branch}/src/core/server/logging/README.md[logging service documentation]. - -[source,typescript] ----- -import type { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; - -export class MyPlugin implements Plugin { - private readonly logger: Logger; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get(); - } - - public setup(core: CoreSetup) { - try { - this.logger.debug('doing something...'); - // … - } catch (e) { - this.logger.error('failed doing something...'); - } - } -} ----- - -==== Elasticsearch service -`Elasticsearch service` provides `elasticsearch.client` program API to communicate with Elasticsearch server REST API. -`elasticsearch.client` interacts with Elasticsearch service on behalf of: - -- `kibana_system` user via `elasticsearch.client.asInternalUser.*` methods. -- a current end-user via `elasticsearch.client.asCurrentUser.*` methods. In this case Elasticsearch client should be given the current user credentials. -See <> and <>. - -{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md[Elasticsearch service API docs] - -[source,typescript] ----- -import { CoreStart, Plugin } from 'kibana/public'; - -export class MyPlugin implements Plugin { - public start(core: CoreStart) { - async function asyncTask() { - const result = await core.elasticsearch.client.asInternalUser.ping(…); - } - asyncTask(); - } -} ----- -For advanced use-cases, such as a search, use {kib-repo}blob/{branch}/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md[Data plugin] -include::saved-objects-service.asciidoc[leveloffset=+1] - -==== HTTP service -Allows plugins: - -* to extend the {kib} server with custom REST API. -* to execute custom logic on an incoming request or server response. -* implement custom authentication and authorization strategy. - -See {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md[HTTP service API docs] - -[source,typescript] ----- -import { schema } from '@kbn/config-schema'; -import type { CoreSetup, Plugin } from 'kibana/server'; - -export class MyPlugin implements Plugin { - public setup(core: CoreSetup) { - const router = core.http.createRouter(); - - const validate = { - params: schema.object({ - id: schema.string(), - }), - }; - - router.get({ - path: 'my_plugin/{id}', - validate - }, - async (context, request, response) => { - const data = await findObject(request.params.id); - if (!data) return response.notFound(); - return response.ok({ - body: data, - headers: { - 'content-type': 'application/json' - } - }); - }); - } -} ----- - -==== UI settings service -The program interface to <>. -It makes it possible for Kibana plugins to extend Kibana UI Settings Management with custom settings. - -See: - -- {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md[UI settings service Setup API docs] -- {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.uisettingsservicestart.register.md[UI settings service Start API docs] - -[source,typescript] ----- -import { schema } from '@kbn/config-schema'; -import type { CoreSetup,Plugin } from 'kibana/server'; - -export class MyPlugin implements Plugin { - public setup(core: CoreSetup) { - core.uiSettings.register({ - custom: { - value: '42', - schema: schema.string(), - }, - }); - const router = core.http.createRouter(); - router.get({ - path: 'my_plugin/{id}', - validate: …, - }, - async (context, request, response) => { - const customSetting = await context.uiSettings.client.get('custom'); - … - }); - } -} - ----- - -=== Client-side -==== Application service -Kibana has migrated to be a Single Page Application. Plugins should use `Application service` API to instruct Kibana what an application should be loaded & rendered in the UI in response to user interactions. -[source,typescript] ----- -import { AppMountParameters, CoreSetup, Plugin, DEFAULT_APP_CATEGORIES } from 'kibana/public'; - -export class MyPlugin implements Plugin { - public setup(core: CoreSetup) { - core.application.register({ // <1> - category: DEFAULT_APP_CATEGORIES.kibana, - id: 'my-plugin', - title: 'my plugin title', - euiIconType: '/path/to/some.svg', - order: 100, - appRoute: '/app/my_plugin', // <2> - async mount(params: AppMountParameters) { // <3> - // Load application bundle - const { renderApp } = await import('./application'); - // Get start services - const [coreStart, depsStart] = await core.getStartServices(); // <4> - // Render the application - return renderApp(coreStart, depsStart, params); // <5> - }, - }); - } -} ----- -<1> See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md[application.register interface] -<2> Application specific URL. -<3> `mount` callback is invoked when a user navigates to the application-specific URL. -<4> `core.getStartServices` method provides API available during `start` lifecycle. -<5> `mount` method must return a function that will be called to unmount the application. - -NOTE:: you are free to use any UI library to render a plugin application in DOM. -However, we recommend using React and https://elastic.github.io/eui[EUI] for all your basic UI -components to create a consistent UI experience. - -==== HTTP service -Provides API to communicate with the {kib} server. Feel free to use another HTTP client library to request 3rd party services. - -[source,typescript] ----- -import { CoreStart } from 'kibana/public'; -interface ResponseType {…}; -async function fetchData(core: CoreStart) { - return await core.http.get<>( - '/api/my_plugin/', - { query: … }, - ); -} ----- -See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.httpsetup.md[for all available API]. - -==== Elasticsearch service -Not available in the browser. Use {kib-repo}blob/{branch}/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md[Data plugin] instead. - -== Patterns -[[scoped-services]] -=== Scoped services -Whenever Kibana needs to get access to data saved in elasticsearch, it -should perform a check whether an end-user has access to the data. In -the legacy platform, Kibana requires binding elasticsearch related API -with an incoming request to access elasticsearch service on behalf of a -user. - -[source,js] ----- -async function handler(req, res) { - const dataCluster = server.plugins.elasticsearch.getCluster('data'); - const data = await dataCluster.callWithRequest(req, 'ping'); -} ----- - -The Kibana Platform introduced a handler interface on the server-side to perform that association -internally. Core services, that require impersonation with an incoming -request, are exposed via `context` argument of -{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandler.md[the -request handler interface.] The above example looks in the Kibana Platform -as - -[source,js] ----- -async function handler(context, req, res) { - const data = await context.core.elasticsearch.client.asCurrentUser('ping'); -} ----- - -The -{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md[request -handler context] exposed the next scoped *core* services: - -[width="100%",cols="30%,70%",options="header",] -|=== -|Legacy Platform |Kibana Platform -|`request.getSavedObjectsClient` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md[`context.savedObjects.client`] - -|`server.plugins.elasticsearch.getCluster('admin')` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md[`context.elasticsearch.client`] - -|`server.plugins.elasticsearch.getCluster('data')` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md[`context.elasticsearch.client`] - -|`request.getUiSettingsService` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md[`context.uiSettings.client`] -|=== - -==== Declare a custom scoped service - -Plugins can extend the handler context with a custom API that will be -available to the plugin itself and all dependent plugins. For example, -the plugin creates a custom elasticsearch client and wants to use it via -the request handler context: - -[source,typescript] ----- -import type { CoreSetup, RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; - -interface MyRequestHandlerContext extends RequestHandlerContext { - myPlugin: { - client: IScopedClusterClient; - }; -} - -class MyPlugin { - setup(core: CoreSetup) { - const client = core.elasticsearch.createClient('myClient'); - core.http.registerRouteHandlerContext('myPlugin', (context, req, res) => { - return { client: client.asScoped(req) }; - }); - const router = core.http.createRouter(); - router.get( - { path: '/api/my-plugin/', validate: … }, - async (context, req, res) => { - // context type is inferred as MyPluginContext - const data = await context.myPlugin.client.asCurrentUser('endpoint'); - } - ); - } ----- diff --git a/docs/developer/architecture/core/logging-configuration-migration.asciidoc b/docs/developer/architecture/core/logging-configuration-migration.asciidoc new file mode 100644 index 0000000000000..19f10a881d5e8 --- /dev/null +++ b/docs/developer/architecture/core/logging-configuration-migration.asciidoc @@ -0,0 +1,84 @@ +[[logging-configuration-migration]] +== Logging configuration migration + +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write +root appenders, make sure that it contains `default` appender to provide backward compatibility. + +NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats. +These will be removed when the `default` appender is no longer required. If you define an appender for a logger, +the log messages aren't handled by the `root` logger anymore and are not forwarded to the legacy logging service. + +[[logging-pattern-format-old-and-new-example]] +[options="header"] +|=== + +| Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format + +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | Absolute `23:33:22.011` + +| logger | `parent.child` | `['parent', 'child']` + +| level | `DEBUG` | `['debug']` + +| meta | stringified JSON object `{"to": "v8"}`| N/A + +| pid | can be configured as `%pid` | N/A + +|=== + +[[logging-json-format-old-and-new-example]] +[options="header"] +|=== + +| Parameter | Platform log record in **json** format | Legacy Platform log record **json** format + +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | ISO8601 `2012-01-31T23:33:22.011Z` + +| logger | `log.logger: parent.child` | `tags: ['parent', 'child']` + +| level | `log.level: DEBUG` | `tags: ['debug']` + +| meta | merged in log record `{... "to": "v8"}` | merged in log record `{... "to": "v8"}` + +| pid | `process.pid: 12345` | `pid: 12345` + +| type | N/A | `type: log` + +| error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` + +|=== + +[[logging-cli-migration]] +=== Logging configuration via CLI + +As is the case for any of Kibana's config settings, you can specify your logging configuration via the CLI. For convenience, the `--verbose` and `--silent` flags exist as shortcuts and will continue to be supported beyond v7. + +If you wish to override these flags, you can always do so by passing your preferred logging configuration directly to the CLI. For example, with the following configuration: + +[source,yaml] +---- +logging: + appenders: + custom: + type: console + layout: + type: pattern + pattern: "[%date][%level] %message" +---- + +you can override the flags with: + +[options="header"] +|=== + +| legacy logging | {kib} Platform logging | cli shortcuts + +|--verbose| --logging.root.level=debug --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | --verbose + +|--quiet| --logging.root.level=error --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | not supported + +|--silent| --logging.root.level=off | --silent +|=== + +NOTE: To preserve backwards compatibility, you are required to pass the root `default` appender until the legacy logging system is removed in `v8.0`. diff --git a/docs/developer/architecture/core/logging-service.asciidoc b/docs/developer/architecture/core/logging-service.asciidoc new file mode 100644 index 0000000000000..7dc2a4ca1f4ce --- /dev/null +++ b/docs/developer/architecture/core/logging-service.asciidoc @@ -0,0 +1,545 @@ +[[logging-service]] +== Logging service +Allows a plugin to provide status and diagnostic information. + +NOTE: The Logging service is only available server side. + +[source,typescript] +---- +import type { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; + +export class MyPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + try { + this.logger.debug('doing something...'); + // … + } catch (e) { + this.logger.error('failed doing something...'); + } + } +} +---- + +The way logging works in {kib} is inspired by the `log4j 2` logging framework used by {ref-bare}/current/logging.html[Elasticsearch]. +The main idea is to have consistent logging behavior (configuration, log format etc.) across the entire Elastic Stack where possible. + +=== Loggers, Appenders and Layouts + +The {kib} logging system has three main components: _loggers_, _appenders_ and _layouts_. These components allow us to log +messages according to message type and level, to control how these messages are formatted and where the final logs +will be displayed or stored. + +__Loggers__ define what logging settings should be applied to a particular logger. + +__<>__ define where log messages are displayed (eg. stdout or console) and stored (eg. file on the disk). + +__<>__ define how log messages are formatted and what type of information they include. + +[[log-level]] +=== Log level + +Currently we support the following log levels: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. + +Levels are ordered, so _all_ > _fatal_ > _error_ > _warn_ > _info_ > _debug_ > _trace_ > _off_. + +A log record is being logged by the logger if its level is higher than or equal to the level of its logger. Otherwise, +the log record is ignored. + +The _all_ and _off_ levels can be used only in configuration and are just handy shortcuts that allow you to log every +log record or disable logging entirely or for a specific logger. These levels are also configurable as <>. + +[[logging-layouts]] +=== Layouts + +Every appender should know exactly how to format log messages before they are written to the console or file on the disk. +This behavior is controlled by the layouts and configured through `appender.layout` configuration property for every +custom appender. Currently we don't define any default layout for the +custom appenders, so one should always make the choice explicitly. + +There are two types of layout supported at the moment: <> and <>. + +[[pattern-layout]] +==== Pattern layout + +With `pattern` layout it's possible to define a string pattern with special placeholders `%conversion_pattern` that will be replaced with data from the actual log message. By default the following pattern is used: `[%date][%level][%logger] %message`. + +NOTE: The `pattern` layout uses a sub-set of https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout[log4j 2 pattern syntax] and **doesn't implement** all `log4j 2` capabilities. + +The conversions that are provided out of the box are: + +**level** +Outputs the <> of the logging event. +Example of `%level` output: `TRACE`, `DEBUG`, `INFO`. + +**logger** +Outputs the name of the logger that published the logging event. +Example of `%logger` output: `server`, `server.http`, `server.http.kibana`. + +**message** +Outputs the application supplied message associated with the logging event. + +**meta** +Outputs the entries of `meta` object data in **json** format, if one is present in the event. +Example of `%meta` output: +[source,bash] +---- +// Meta{from: 'v7', to: 'v8'} +'{"from":"v7","to":"v8"}' +// Meta empty object +'{}' +// no Meta provided +'' +---- + +[[date-format]] +**date** +Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. +Timezone name is expected to be one from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[TZ database name]. +Timezone defaults to the host timezone when not explicitly specified. +Example of `%date` output: + +[[date-conversion-pattern-examples]] +[options="header"] +|=== + +| Conversion pattern | Example + +| `%date` +| `2012-02-01T14:30:22.011Z` uses `ISO8601` format by default + +| `%date{ISO8601}` +| `2012-02-01T14:30:22.011Z` + +| `%date{ISO8601_TZ}` +| `2012-02-01T09:30:22.011-05:00` `ISO8601` with timezone + +| `%date{ISO8601_TZ}{America/Los_Angeles}` +| `2012-02-01T06:30:22.011-08:00` + +| `%date{ABSOLUTE}` +| `09:30:22.011` + +| `%date{ABSOLUTE}{America/Los_Angeles}` +| `06:30:22.011` + +| `%date{UNIX}` +| `1328106622` + +| `%date{UNIX_MILLIS}` +| `1328106622011` + +|=== + +**pid** +Outputs the process ID. + +The pattern layout also offers a `highlight` option that allows you to highlight +some parts of the log message with different colors. Highlighting is quite handy if log messages are forwarded +to a terminal with color support. + +[[json-layout]] +==== JSON layout +With `json` layout log messages will be formatted as JSON strings in https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[ECS format] that includes a timestamp, log level, logger, message text and any other metadata that may be associated with the log message itself. + +[[logging-appenders]] +=== Appenders + +[[rolling-file-appender]] +==== Rolling File Appender + +Similar to Log4j's `RollingFileAppender`, this appender will log into a file, and rotate it following a rolling +strategy when the configured policy triggers. + +===== Triggering Policies + +The triggering policy determines when a rollover should occur. + +There are currently two policies supported: `size-limit` and `time-interval`. + +[[size-limit-triggering-policy]] +**SizeLimitTriggeringPolicy** + +This policy will rotate the file when it reaches a predetermined size. + +[source,yaml] +---- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + type: size-limit + size: 50mb + strategy: + //... + layout: + type: pattern +---- + +The options are: + +- `size` + +The maximum size the log file should reach before a rollover should be performed. The default value is `100mb` + +[[time-interval-triggering-policy]] +**TimeIntervalTriggeringPolicy** + +This policy will rotate the file every given interval of time. + +[source,yaml] +---- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + type: time-interval + interval: 10s + modulate: true + strategy: + //... + layout: + type: pattern +---- + +The options are: + +- `interval` + +How often a rollover should occur. The default value is `24h` + +- `modulate` + +Whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. + +For example, if modulate is true and the interval is `4h`, if the current hour is 3 am then the first rollover will occur at 4 am +and then next ones will occur at 8 am, noon, 4pm, etc. The default value is `true`. + +===== Rolling strategies + +The rolling strategy determines how the rollover should occur: both the naming of the rolled files, +and their retention policy. + +There is currently one strategy supported: `numeric`. + +**NumericRollingStrategy** + +This strategy will suffix the file with a given pattern when rolling, +and will retains a fixed amount of rolled files. + +[source,yaml] +---- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + // ... + strategy: + type: numeric + pattern: '-%i' + max: 2 + layout: + type: pattern +---- + +For example, with this configuration: + +- During the first rollover kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts + being written to. +- During the second rollover kibana-1.log is renamed to kibana-2.log and kibana.log is renamed to kibana-1.log. + A new kibana.log file is created and starts being written to. +- During the third and subsequent rollovers, kibana-2.log is deleted, kibana-1.log is renamed to kibana-2.log and + kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts being written to. + +The options are: + +- `pattern` + +The suffix to append to the file path when rolling. Must include `%i`, as this is the value +that will be converted to the file index. + +For example, with `fileName: /var/logs/kibana.log` and `pattern: '-%i'`, the rolling files created +will be `/var/logs/kibana-1.log`, `/var/logs/kibana-2.log`, and so on. The default value is `-%i` + +- `max` + +The maximum number of files to keep. Once this number is reached, oldest files will be deleted. The default value is `7` + +==== Rewrite Appender + +WARNING: This appender is currently considered experimental and is not intended +for public consumption. The API is subject to change at any time. + +Similar to log4j's `RewriteAppender`, this appender serves as a sort of middleware, +modifying the provided log events before passing them along to another +appender. + +[source,yaml] +---- +logging: + appenders: + my-rewrite-appender: + type: rewrite + appenders: [console, file] # name of "destination" appender(s) + policy: + # ... +---- + +The most common use case for the `RewriteAppender` is when you want to +filter or censor sensitive data that may be contained in a log entry. +In fact, with a default configuration, {kib} will automatically redact +any `authorization`, `cookie`, or `set-cookie` headers when logging http +requests & responses. + +To configure additional rewrite rules, you'll need to specify a <>. + +[[rewrite-policies]] +===== Rewrite Policies + +Rewrite policies exist to indicate which parts of a log record can be +modified within the rewrite appender. + +**Meta** + +The `meta` rewrite policy can read and modify any data contained in the +`LogMeta` before passing it along to a destination appender. + +Meta policies must specify one of three modes, which indicate which action +to perform on the configured properties: +- `update` updates an existing property at the provided `path`. +- `remove` removes an existing property at the provided `path`. + +The `properties` are listed as a `path` and `value` pair, where `path` is +the dot-delimited path to the target property in the `LogMeta` object, and +`value` is the value to add or update in that target property. When using +the `remove` mode, a `value` is not necessary. + +Here's an example of how you would replace any `cookie` header values with `[REDACTED]`: + +[source,yaml] +---- +logging: + appenders: + my-rewrite-appender: + type: rewrite + appenders: [console] + policy: + type: meta # indicates that we want to rewrite the LogMeta + mode: update # will update an existing property only + properties: + - path: "http.request.headers.cookie" # path to property + value: "[REDACTED]" # value to replace at path +---- + +Rewrite appenders can even be passed to other rewrite appenders to apply +multiple filter policies/modes, as long as it doesn't create a circular +reference. Each rewrite appender is applied sequentially (one after the other). + +[source,yaml] +---- +logging: + appenders: + remove-request-headers: + type: rewrite + appenders: [censor-response-headers] # redirect to the next rewrite appender + policy: + type: meta + mode: remove + properties: + - path: "http.request.headers" # remove all request headers + censor-response-headers: + type: rewrite + appenders: [console] # output to console + policy: + type: meta + mode: update + properties: + - path: "http.response.headers.set-cookie" + value: "[REDACTED]" +---- + +===== Complete Example For Rewrite Appender + +[source,yaml] +---- +logging: + appenders: + custom_console: + type: console + layout: + type: pattern + highlight: true + pattern: "[%date][%level][%logger] %message %meta" + file: + type: file + fileName: ./kibana.log + layout: + type: json + censor: + type: rewrite + appenders: [custom_console, file] + policy: + type: meta + mode: update + properties: + - path: "http.request.headers.cookie" + value: "[REDACTED]" + loggers: + - name: http.server.response + appenders: [censor] # pass these logs to our rewrite appender + level: debug +---- + +[[logger-hierarchy]] +=== Logger hierarchy + +Every logger has a unique name that follows a hierarchical naming rule. The logger is considered to be an +ancestor of another logger if its name followed by a `.` is a prefix of the descendant logger. For example, a logger +named `a.b` is an ancestor of logger `a.b.c`. All top-level loggers are descendants of a special `root` logger at the top of the logger hierarchy. The `root` logger always exists and +fully configured. + +You can configure _<>_ and _appenders_ for a specific logger. If a logger only has a _log level_ configured, then the _appenders_ configuration applied to the logger is inherited from the ancestor logger. + +NOTE: In the current implementation we __don't support__ so called _appender additivity_ when log messages are forwarded to _every_ distinct appender within the +ancestor chain including `root`. That means that log messages are only forwarded to appenders that are configured for a particular logger. If a logger doesn't have any appenders configured, the configuration of that particular logger will be inherited from its closest ancestor. + +[[dedicated-loggers]] +==== Dedicated loggers + +**Root** + +The `root` logger has a dedicated configuration node since this logger is special and should always exist. By default `root` is configured with `info` level and `default` appender that is also always available. This is the configuration that all custom loggers will use unless they're re-configured explicitly. + +For example to see _all_ log messages that fall back on the `root` logger configuration, just add one line to the configuration: + +[source,yaml] +---- +logging.root.level: all +---- + +Or disable logging entirely with `off`: + +[source,yaml] +---- +logging.root.level: off +---- + +**Metrics Logs** + +The `metrics.ops` logger is configured with `debug` level and will automatically output sample system and process information at a regular interval. +The metrics that are logged are a subset of the data collected and are formatted in the log message as follows: + +[options="header"] +|=== + +| Ops formatted log property | Location in metrics service | Log units + +| memory | process.memory.heap.used_in_bytes | http://numeraljs.com/#format[depends on the value], typically MB or GB + +| uptime | process.uptime_in_millis | HH:mm:ss + +| load | os.load | [ "load for the last 1 min" "load for the last 5 min" "load for the last 15 min"] + +| delay | process.event_loop_delay | ms +|=== + +The log interval is the same as the interval at which system and process information is refreshed and is configurable under `ops.interval`: + +[source,yaml] +---- +ops.interval: 5000 +---- + +The minimum interval is 100ms and defaults to 5000ms. + +[[request-response-logger]] +**Request and Response Logs** + +The `http.server.response` logger is configured with `debug` level and will automatically output +data about http requests and responses occurring on the {kib} server. +The message contains some high-level information, and the corresponding log meta contains the following: + +[options="header"] +|=== + +| Meta property | Description | Format + +| client.ip | IP address of the requesting client | ip + +| http.request.method | http verb for the request (uppercase) | string + +| http.request.mime_type | (optional) mime as specified in the headers | string + +| http.request.referrer | (optional) referrer | string + +| http.request.headers | request headers | object + +| http.response.body.bytes | (optional) Calculated response payload size in bytes | number + +| http.response.status_code | status code returned | number + +| http.response.headers | response headers | object + +| http.response.responseTime | (optional) Calculated response time in ms | number + +| url.path | request path | string + +| url.query | (optional) request query string | string + +| user_agent.original | raw user-agent string provided in request headers | string + +|=== + +=== Usage + +Usage is very straightforward, one should just get a logger for a specific context and use it to log messages with +different log level. + +[source,typescript] +---- +const logger = kibana.logger.get('server'); + +logger.trace('Message with `trace` log level.'); +logger.debug('Message with `debug` log level.'); +logger.info('Message with `info` log level.'); +logger.warn('Message with `warn` log level.'); +logger.error('Message with `error` log level.'); +logger.fatal('Message with `fatal` log level.'); + +const loggerWithNestedContext = kibana.logger.get('server', 'http'); +loggerWithNestedContext.trace('Message with `trace` log level.'); +loggerWithNestedContext.debug('Message with `debug` log level.'); +---- + +And assuming logger for `server` name with `console` appender and `trace` level was used, console output will look like this: +[source,bash] +---- +[2017-07-25T11:54:41.639-07:00][TRACE][server] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server] Message with `debug` log level. +[2017-07-25T11:54:41.639-07:00][INFO ][server] Message with `info` log level. +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. + +[2017-07-25T11:54:41.639-07:00][TRACE][server.http] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server.http] Message with `debug` log level. +---- + +The log will be less verbose with `warn` level for the `server` logger: +[source,bash] +---- +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. +---- diff --git a/docs/developer/architecture/core/patterns-scoped-services.asciidoc b/docs/developer/architecture/core/patterns-scoped-services.asciidoc new file mode 100644 index 0000000000000..d4618684fc7e4 --- /dev/null +++ b/docs/developer/architecture/core/patterns-scoped-services.asciidoc @@ -0,0 +1,61 @@ +[[patterns]] +== Patterns +[[scoped-services]] +=== Scoped services +Whenever Kibana needs to get access to data saved in Elasticsearch, it +should perform a check whether an end-user has access to the data. +The Kibana Platform introduced a handler interface on the server-side to perform that association +internally. Core services, that require impersonation with an incoming +request, are exposed via `context` argument of +{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandler.md[the +request handler interface.] +as + +[source,js] +---- +async function handler(context, req, res) { + const data = await context.core.elasticsearch.client.asCurrentUser('ping'); +} +---- + +The +{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md[request +handler context] exposes the following scoped *core* services: + +* {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md[`context.savedObjects.client`] +* {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md[`context.elasticsearch.client`] +* {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md[`context.uiSettings.client`] + +==== Declare a custom scoped service + +Plugins can extend the handler context with a custom API that will be +available to the plugin itself and all dependent plugins. For example, +the plugin creates a custom Elasticsearch client and wants to use it via +the request handler context: + +[source,typescript] +---- +import type { CoreSetup, RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; + +interface MyRequestHandlerContext extends RequestHandlerContext { + myPlugin: { + client: IScopedClusterClient; + }; +} + +class MyPlugin { + setup(core: CoreSetup) { + const client = core.elasticsearch.createClient('myClient'); + core.http.registerRouteHandlerContext('myPlugin', (context, req, res) => { + return { client: client.asScoped(req) }; + }); + const router = core.http.createRouter(); + router.get( + { path: '/api/my-plugin/', validate: … }, + async (context, req, res) => { + // context type is inferred as MyPluginContext + const data = await context.myPlugin.client.asCurrentUser('endpoint'); + } + ); + } +---- diff --git a/docs/developer/architecture/core/saved-objects-service.asciidoc b/docs/developer/architecture/core/saved-objects-service.asciidoc index 047c3dffa6358..fa7fc4233259d 100644 --- a/docs/developer/architecture/core/saved-objects-service.asciidoc +++ b/docs/developer/architecture/core/saved-objects-service.asciidoc @@ -1,6 +1,8 @@ [[saved-objects-service]] == Saved Objects service +NOTE: The Saved Objects service is available both server and client side. + `Saved Objects service` allows {kib} plugins to use {es} like a primary database. Think of it as an Object Document Mapper for {es}. Once a plugin has registered one or more Saved Object types, the Saved Objects client @@ -28,7 +30,9 @@ spaces. This document contains developer guidelines and best-practices for plugins wanting to use Saved Objects. -=== Registering a Saved Object type +=== Server side usage + +==== Registering a Saved Object type Saved object type definitions should be defined in their own `my_plugin/server/saved_objects` directory. The folder should contain a file per type, named after the snake_case name of the type, and an `index.ts` file exporting all the types. @@ -83,7 +87,7 @@ export class MyPlugin implements Plugin { } ---- -=== Mappings +==== Mappings Each Saved Object type can define it's own {es} field mappings. Because multiple Saved Object types can share the same index, mappings defined by a type will be nested under a top-level field that matches the type name. @@ -149,59 +153,6 @@ should carefully consider the fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the `.kibana` index. -=== References -When a Saved Object declares `references` to other Saved Objects, the -Saved Objects Export API will automatically export the target object with all -of it's references. This makes it easy for users to export the entire -reference graph of an object. - -If a Saved Object can't be used on it's own, that is, it needs other objects -to exist for a feature to function correctly, that Saved Object should declare -references to all the objects it requires. For example, a `dashboard` -object might have panels for several `visualization` objects. When these -`visualization` objects don't exist, the dashboard cannot be rendered -correctly. The `dashboard` object should declare references to all it's -visualizations. - -However, `visualization` objects can continue to be rendered or embedded into -other dashboards even if the `dashboard` it was originally embedded into -doesn't exist. As a result, `visualization` objects should not declare -references to `dashboard` objects. - -For each referenced object, an `id`, `type` and `name` are added to the -`references` array: - -[source, typescript] ----- -router.get( - { path: '/some-path', validate: false }, - async (context, req, res) => { - const object = await context.core.savedObjects.client.create( - 'dashboard', - { - title: 'my dashboard', - panels: [ - { visualization: 'vis1' }, // <1> - ], - indexPattern: 'indexPattern1' - }, - { references: [ - { id: '...', type: 'visualization', name: 'vis1' }, - { id: '...', type: 'index_pattern', name: 'indexPattern1' }, - ] - } - ) - ... - } -); ----- -<1> Note how `dashboard.panels[0].visualization` stores the `name` property of -the reference (not the `id` directly) to be able to uniquely identify this -reference. This guarantees that the id the reference points to always remains -up to date. If a visualization `id` was directly stored in -`dashboard.panels[0].visualization` there is a risk that this `id` gets -updated without updating the reference in the references array. - ==== Writing Migrations Saved Objects support schema changes between Kibana versions, which we call @@ -308,4 +259,60 @@ point in time. It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a bug -in this code, there's really no reason not to aim for 100% test code coverage. \ No newline at end of file +in this code, there's really no reason not to aim for 100% test code coverage. + +=== Client side usage + +==== References + +When a Saved Object declares `references` to other Saved Objects, the +Saved Objects Export API will automatically export the target object with all +of its references. This makes it easy for users to export the entire +reference graph of an object. + +If a Saved Object can't be used on its own, that is, it needs other objects +to exist for a feature to function correctly, that Saved Object should declare +references to all the objects it requires. For example, a `dashboard` +object might have panels for several `visualization` objects. When these +`visualization` objects don't exist, the dashboard cannot be rendered +correctly. The `dashboard` object should declare references to all its +visualizations. + +However, `visualization` objects can continue to be rendered or embedded into +other dashboards even if the `dashboard` it was originally embedded into +doesn't exist. As a result, `visualization` objects should not declare +references to `dashboard` objects. + +For each referenced object, an `id`, `type` and `name` are added to the +`references` array: + +[source, typescript] +---- +router.get( + { path: '/some-path', validate: false }, + async (context, req, res) => { + const object = await context.core.savedObjects.client.create( + 'dashboard', + { + title: 'my dashboard', + panels: [ + { visualization: 'vis1' }, // <1> + ], + indexPattern: 'indexPattern1' + }, + { references: [ + { id: '...', type: 'visualization', name: 'vis1' }, + { id: '...', type: 'index_pattern', name: 'indexPattern1' }, + ] + } + ) + ... + } +); +---- +<1> Note how `dashboard.panels[0].visualization` stores the `name` property of +the reference (not the `id` directly) to be able to uniquely identify this +reference. This guarantees that the id the reference points to always remains +up to date. If a visualization `id` was directly stored in +`dashboard.panels[0].visualization` there is a risk that this `id` gets +updated without updating the reference in the references array. diff --git a/docs/developer/architecture/core/uisettings-service.asciidoc b/docs/developer/architecture/core/uisettings-service.asciidoc new file mode 100644 index 0000000000000..85ed9c9eabc72 --- /dev/null +++ b/docs/developer/architecture/core/uisettings-service.asciidoc @@ -0,0 +1,40 @@ +[[ui-settings-service]] +== UI settings service + +NOTE: The UI settings service is available both server and client side. + +=== Server side usage + +The program interface to <>. +It makes it possible for Kibana plugins to extend Kibana UI Settings Management with custom settings. + +See: + +- {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md[UI settings service Setup API docs] + +[source,typescript] +---- +import { schema } from '@kbn/config-schema'; +import type { CoreSetup,Plugin } from 'kibana/server'; + +export class MyPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register({ + custom: { + value: '42', + schema: schema.string(), + }, + }); + const router = core.http.createRouter(); + router.get({ + path: 'my_plugin/{id}', + validate: …, + }, + async (context, request, response) => { + const customSetting = await context.uiSettings.client.get('custom'); + … + }); + } +} + +---- diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index 4bdd693979b49..1a0e7bab2f8f8 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -29,6 +29,24 @@ include::kibana-platform-plugin-api.asciidoc[leveloffset=+1] include::core/index.asciidoc[leveloffset=+1] +include::core/application_service.asciidoc[leveloffset=+1] + +include::core/configuration-service.asciidoc[leveloffset=+1] + +include::core/elasticsearch-service.asciidoc[leveloffset=+1] + +include::core/http-service.asciidoc[leveloffset=+1] + +include::core/logging-service.asciidoc[leveloffset=+1] + +include::core/logging-configuration-migration.asciidoc[leveloffset=+1] + +include::core/saved-objects-service.asciidoc[leveloffset=+1] + +include::core/uisettings-service.asciidoc[leveloffset=+1] + +include::core/patterns-scoped-services.asciidoc[leveloffset=+1] + include::security/index.asciidoc[leveloffset=+1] include::add-data-tutorials.asciidoc[leveloffset=+1] diff --git a/docs/developer/contributing/index.asciidoc b/docs/developer/contributing/index.asciidoc index bbf2903491bf6..1cf96d19bfb2b 100644 --- a/docs/developer/contributing/index.asciidoc +++ b/docs/developer/contributing/index.asciidoc @@ -1,7 +1,7 @@ [[contributing]] == Contributing -Whether you want to fix a bug, implement a feature, or add some other improvements or apis, the following sections will +Whether you want to fix a bug, implement a feature, add an improvement, or add APIs, the following sections will guide you on the process. After committing your code, check out the link:https://www.elastic.co/community/contributor[Elastic Contributor Program] where you can earn points and rewards for your contributions. Read <> to get your environment up and running, then read <>. @@ -53,24 +53,27 @@ To use a single paragraph of text, enter a `Release note:` or `## Release note` When you create the Release Notes text, use the following best practices: -* Use present tense. +* Use active voice. * Use sentence case. -* When you create a feature PR, start with `Adds`. -* When you create an enhancement PR, start with `Improves`. -* When you create a bug fix PR, start with `Fixes`. -* When you create a deprecation PR, start with `Deprecates`. +* When you create a PR that adds a feature, start with `Adds`. +* When you create a PR that improves an existing feature, start with `Improves`. +* When you create a PR that fixes existing functionality, start with `Fixes`. +* When you create a PR that deprecates functionality, start with `Deprecates`. [discrete] ==== Add your labels +To make sure that your PR is included in the Release Notes, add the right label. + [arabic] . Label the PR with the targeted version (ex: `v7.3.0`). . Label the PR with the appropriate GitHub labels: - * For a new feature or functionality, use `release_note:enhancement`. - * For an external-facing fix, use `release_note:fix`. We do not include docs, build, and test fixes in the Release Notes, or unreleased issues that are only on `master`. - * For a deprecated feature, use `release_note:deprecation`. - * For a breaking change, use `release_note:breaking`. - * To **NOT** include your changes in the Release Notes, use `release_note:skip`. + * `release_note:feature` — New user-facing features, significant enhancements to features, and significant bug fixes (in rare cases). + * `release_note:enhancement` — Minor UI changes and enhancements. + * `release_note:fix` — Fixes for bugs that existed in the previous release. + * `release_note:deprecation` — Deprecates functionality that existed in previous releases. + * `release_note:breaking` — Breaking changes that weren't present in previous releases. + * `release_note:skip` — Changes that should not appear in the Release Notes. For example, docs, build, and test fixes, or unreleased issues that are only in `master`. include::development-github.asciidoc[leveloffset=+1] diff --git a/docs/developer/telemetry.asciidoc b/docs/developer/telemetry.asciidoc index fe2bf5f957379..c478c091c1c10 100644 --- a/docs/developer/telemetry.asciidoc +++ b/docs/developer/telemetry.asciidoc @@ -8,6 +8,7 @@ The operations we current report timing data for: * Total execution time of `yarn kbn bootstrap`. * Total execution time of `@kbn/optimizer` runs as well as the following metadata about the runs: The number of bundles created, the number of bundles which were cached, usage of `--watch`, `--dist`, `--workers` and `--no-cache` flags, and the count of themes being built. * The time from when you run `yarn start` until both the Kibana server and `@kbn/optimizer` are ready for use. +* The time it takes for the Kibana server to start listening after it is spawned by `yarn start`. Along with the execution time of each execution, we ship the following information about your machine to the service: diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.deprecations.md b/docs/development/core/public/kibana-plugin-core-public.corestart.deprecations.md new file mode 100644 index 0000000000000..624c4868d54a7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) + +## CoreStart.deprecations property + +[DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) + +Signature: + +```typescript +deprecations: DeprecationsServiceStart; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index a7b45b318d2c9..6ad9adca53ef5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -18,6 +18,7 @@ export interface CoreStart | --- | --- | --- | | [application](./kibana-plugin-core-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | +| [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | | [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md new file mode 100644 index 0000000000000..8175da8a1893a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [getAllDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md) + +## DeprecationsServiceStart.getAllDeprecations property + +Grabs deprecations details for all domains. + +Signature: + +```typescript +getAllDeprecations: () => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md new file mode 100644 index 0000000000000..6e3472b7c3fe3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [getDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md) + +## DeprecationsServiceStart.getDeprecations property + +Grabs deprecations for a specific domain. + +Signature: + +```typescript +getDeprecations: (domainId: string) => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md new file mode 100644 index 0000000000000..842761f6b7cea --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [isDeprecationResolvable](./kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md) + +## DeprecationsServiceStart.isDeprecationResolvable property + +Returns a boolean if the provided deprecation can be automatically resolvable. + +Signature: + +```typescript +isDeprecationResolvable: (details: DomainDeprecationDetails) => boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md new file mode 100644 index 0000000000000..0d2c963ec5547 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) + +## DeprecationsServiceStart interface + +DeprecationsService provides methods to fetch domain deprecation details from the Kibana server. + +Signature: + +```typescript +export interface DeprecationsServiceStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getAllDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md) | () => Promise<DomainDeprecationDetails[]> | Grabs deprecations details for all domains. | +| [getDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md) | (domainId: string) => Promise<DomainDeprecationDetails[]> | Grabs deprecations for a specific domain. | +| [isDeprecationResolvable](./kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md) | (details: DomainDeprecationDetails) => boolean | Returns a boolean if the provided deprecation can be automatically resolvable. | +| [resolveDeprecation](./kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md) | (details: DomainDeprecationDetails) => Promise<ResolveDeprecationResponse> | Calls the correctiveActions.api to automatically resolve the depprecation. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md new file mode 100644 index 0000000000000..fae623fed3cc2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [resolveDeprecation](./kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md) + +## DeprecationsServiceStart.resolveDeprecation property + +Calls the correctiveActions.api to automatically resolve the depprecation. + +Signature: + +```typescript +resolveDeprecation: (details: DomainDeprecationDetails) => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index df5ce62cc07af..6ca7a83ac0a03 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -87,7 +87,9 @@ readonly links: { readonly sum: string; readonly top_hits: string; }; - readonly runtimeFields: string; + readonly runtimeFields: { + readonly mapping: string; + }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index da3ae17171c81..3847ab0c6183a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index e9d08dcd3bf4c..32f17d5488f66 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -59,6 +59,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeUserBanner](./kibana-plugin-core-public.chromeuserbanner.md) | | | [CoreSetup](./kibana-plugin-core-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | | [CoreStart](./kibana-plugin-core-public.corestart.md) | Core services exposed to the Plugin start lifecycle | +| [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | DeprecationsService provides methods to fetch domain deprecation details from the Kibana server. | | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. | | [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | @@ -164,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) | Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) | | [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | +| [ResolveDeprecationResponse](./kibana-plugin-core-public.resolvedeprecationresponse.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedeprecationresponse.md b/docs/development/core/public/kibana-plugin-core-public.resolvedeprecationresponse.md new file mode 100644 index 0000000000000..928bf8c07004e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedeprecationresponse.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolveDeprecationResponse](./kibana-plugin-core-public.resolvedeprecationresponse.md) + +## ResolveDeprecationResponse type + +Signature: + +```typescript +export declare type ResolveDeprecationResponse = { + status: 'ok'; +} | { + status: 'fail'; + reason: string; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 69cfb818561e5..7be45c6c173b4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -27,10 +27,10 @@ export interface SavedObjectsFindOptions | [preference](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | | [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | -| [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | unknown[] | Use the sort values from the previous page to retrieve the next page of results. | +| [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | estypes.Id[] | Use the sort values from the previous page to retrieve the next page of results. | | [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | -| [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | string | | +| [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | estypes.SortOrder | | | [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | string | string[] | | | [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md index 99ca2c34e77be..7016e1f1b72de 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md @@ -9,5 +9,5 @@ Use the sort values from the previous page to retrieve the next page of results. Signature: ```typescript -searchAfter?: unknown[]; +searchAfter?: estypes.Id[]; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md index 3834c802fa184..36f99e51ea8c6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md @@ -7,5 +7,5 @@ Signature: ```typescript -sortOrder?: string; +sortOrder?: estypes.SortOrder; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.deprecations.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.deprecations.md new file mode 100644 index 0000000000000..436cc29b6e343 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [deprecations](./kibana-plugin-core-server.coresetup.deprecations.md) + +## CoreSetup.deprecations property + +[DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) + +Signature: + +```typescript +deprecations: DeprecationsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 1171dbad570ce..b37ac80db87d6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -18,6 +18,7 @@ export interface CoreSetupCapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | | [context](./kibana-plugin-core-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | +| [deprecations](./kibana-plugin-core-server.coresetup.deprecations.md) | DeprecationsServiceSetup | [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md new file mode 100644 index 0000000000000..e362bc4e0329c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) + +## DeprecationsDetails.correctiveActions property + +Signature: + +```typescript +correctiveActions: { + api?: { + path: string; + method: 'POST' | 'PUT'; + body?: { + [key: string]: any; + }; + }; + manualSteps?: string[]; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md new file mode 100644 index 0000000000000..467d6d76cf842 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) + +## DeprecationsDetails.documentationUrl property + +Signature: + +```typescript +documentationUrl?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md new file mode 100644 index 0000000000000..64ad22e2c87fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [level](./kibana-plugin-core-server.deprecationsdetails.level.md) + +## DeprecationsDetails.level property + +levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. + +Signature: + +```typescript +level: 'warning' | 'critical' | 'fetch_error'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md new file mode 100644 index 0000000000000..bb77e4247711f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) + +## DeprecationsDetails interface + +Signature: + +```typescript +export interface DeprecationsDetails +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps?: string[];
} | | +| [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | | +| [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | +| [message](./kibana-plugin-core-server.deprecationsdetails.message.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md new file mode 100644 index 0000000000000..d79a4c9bd7995 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [message](./kibana-plugin-core-server.deprecationsdetails.message.md) + +## DeprecationsDetails.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md new file mode 100644 index 0000000000000..7d9d3dcdda4da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -0,0 +1,95 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) + +## DeprecationsServiceSetup interface + +The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version. + +The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience. + +If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. + +Signature: + +```typescript +export interface DeprecationsServiceSetup +``` + +## Example + + +```ts +import { DeprecationsDetails, GetDeprecationsContext, CoreSetup } from 'src/core/server'; + +async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { + const deprecations: DeprecationsDetails[] = []; + const count = await getTimelionSheetsCount(savedObjectsClient); + + if (count > 0) { + // Example of a manual correctiveAction + deprecations.push({ + message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + documentationUrl: + 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', + level: 'warning', + correctiveActions: { + manualSteps: [ + 'Navigate to the Kibana Dashboard and click "Create dashboard".', + 'Select Timelion from the "New Visualization" window.', + 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', + 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', + 'In the toolbar, click Save.', + 'On the Save visualization window, enter the visualization Title, then click Save and return.', + ], + }, + }); + } + + // Example of an api correctiveAction + deprecations.push({ + "message": "User 'test_dashboard_user' is using a deprecated role: 'kibana_user'", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", + "level": "critical", + "correctiveActions": { + "api": { + "path": "/internal/security/users/test_dashboard_user", + "method": "POST", + "body": { + "username": "test_dashboard_user", + "roles": [ + "machine_learning_user", + "enrich_user", + "kibana_admin" + ], + "full_name": "Alison Goryachev", + "email": "alisongoryachev@gmail.com", + "metadata": {}, + "enabled": true + } + }, + "manualSteps": [ + "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", + "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role." + ] + }, + }); + + return deprecations; +} + + +export class Plugin() { + setup: (core: CoreSetup) => { + core.deprecations.registerDeprecations({ getDeprecations }); + } +} + +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [registerDeprecations](./kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md) | (deprecationContext: RegisterDeprecationsConfig) => void | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md new file mode 100644 index 0000000000000..07c2a3ad0ce55 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) > [registerDeprecations](./kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md) + +## DeprecationsServiceSetup.registerDeprecations property + +Signature: + +```typescript +registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.esclient.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.esclient.md new file mode 100644 index 0000000000000..70c1864bf905f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.esclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) > [esClient](./kibana-plugin-core-server.getdeprecationscontext.esclient.md) + +## GetDeprecationsContext.esClient property + +Signature: + +```typescript +esClient: IScopedClusterClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md new file mode 100644 index 0000000000000..1018444f0849a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) + +## GetDeprecationsContext interface + +Signature: + +```typescript +export interface GetDeprecationsContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [esClient](./kibana-plugin-core-server.getdeprecationscontext.esclient.md) | IScopedClusterClient | | +| [savedObjectsClient](./kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md) | SavedObjectsClientContract | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md new file mode 100644 index 0000000000000..66da52d3b5824 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) > [savedObjectsClient](./kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md) + +## GetDeprecationsContext.savedObjectsClient property + +Signature: + +```typescript +savedObjectsClient: SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.close.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.close.md new file mode 100644 index 0000000000000..f7cfab446eeca --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.close.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) > [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) + +## ISavedObjectsPointInTimeFinder.close property + +Closes the Point-In-Time associated with this finder instance. + +Once you have retrieved all of the results you need, it is recommended to call `close()` to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to `find` fails for any reason. + +Signature: + +```typescript +close: () => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md new file mode 100644 index 0000000000000..1755ff40c2bc0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) > [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) + +## ISavedObjectsPointInTimeFinder.find property + +An async generator which wraps calls to `savedObjectsClient.find` and iterates over multiple pages of results using `_pit` and `search_after`. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated `perPage` size. + +Signature: + +```typescript +find: () => AsyncGenerator; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md new file mode 100644 index 0000000000000..4686df18e0134 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) + +## ISavedObjectsPointInTimeFinder interface + + +Signature: + +```typescript +export interface ISavedObjectsPointInTimeFinder +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | () => Promise<void> | Closes the Point-In-Time associated with this finder instance.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | +| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index 551cbe3c93750..395c26a6e4bf6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -10,10 +10,10 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | Buffer | Error | Stream | { + custom: | Error | Buffer | { message: string | Error; attributes?: Record | undefined; - } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | Stream | 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.md b/docs/development/core/server/kibana-plugin-core-server.md index 8dd4667002ead..faac8108de825 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -69,13 +69,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) | | | [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) | | | [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) | | +| [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) | | | [DeprecationSettings](./kibana-plugin-core-server.deprecationsettings.md) | UiSettings deprecation field options. | +| [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version.The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience.If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. | | [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | | [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) | | | [GetResponse](./kibana-plugin-core-server.getresponse.md) | | | [HttpAuth](./kibana-plugin-core-server.httpauth.md) | | | [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. | @@ -98,6 +101,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | +| [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) | | | [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | @@ -127,6 +131,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | +| [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) | | | [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) | Defines a set of additional options for the resolveCapabilities method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md). | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | @@ -158,6 +163,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | +| [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | | [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | | [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | | @@ -305,6 +311,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md) | | +| [SavedObjectsCreatePointInTimeFinderOptions](./kibana-plugin-core-server.savedobjectscreatepointintimefinderoptions.md) | | | [SavedObjectsExportTransform](./kibana-plugin-core-server.savedobjectsexporttransform.md) | Transformation function used to mutate the exported objects of the associated type.A type's export transform function will be executed once per user-initiated export, for all objects of that type. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) | A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. | diff --git a/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md new file mode 100644 index 0000000000000..cf008725ff15b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) > [getDeprecations](./kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md) + +## RegisterDeprecationsConfig.getDeprecations property + +Signature: + +```typescript +getDeprecations: (context: GetDeprecationsContext) => MaybePromise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md new file mode 100644 index 0000000000000..59e6d406f84bf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) + +## RegisterDeprecationsConfig interface + +Signature: + +```typescript +export interface RegisterDeprecationsConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getDeprecations](./kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md) | (context: GetDeprecationsContext) => MaybePromise<DeprecationsDetails[]> | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md index dc765260a08ca..79c7d18adf306 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md @@ -6,6 +6,8 @@ Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). +Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md new file mode 100644 index 0000000000000..8afd963464574 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md @@ -0,0 +1,53 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [createPointInTimeFinder](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) + +## SavedObjectsClient.createPointInTimeFinder() method + +Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any `find` queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client. + +Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments. + +The generator wraps calls to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) and iterates over multiple pages of results using `_pit` and `search_after`. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated `perPage`. + +Once you have retrieved all of the results you need, it is recommended to call `close()` to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to `find` fails for any reason. + +Signature: + +```typescript +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| findOptions | SavedObjectsCreatePointInTimeFinderOptions | | +| dependencies | SavedObjectsCreatePointInTimeFinderDependencies | | + +Returns: + +`ISavedObjectsPointInTimeFinder` + +## Example + + +```ts +const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { + type: 'visualization', + search: 'foo*', + perPage: 100, +}; + +const finder = savedObjectsClient.createPointInTimeFinder(findOptions); + +const responses: SavedObjectFindResponse[] = []; +for await (const response of finder.find()) { + responses.push(...response); + if (doneSearching) { + await finder.close(); + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 887f7f7d93a87..95c2251f72c90 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -30,13 +30,14 @@ The constructor for this class is marked as internal. Third-party code should no | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | -| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). | +| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md).Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | +| [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.The generator wraps calls to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | -| [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT. | +| [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md index 56c1d6d1ddc33..c76159ffa5032 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md @@ -6,6 +6,8 @@ Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned `id` can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT. +Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md new file mode 100644 index 0000000000000..95ab9e225c049 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) > [client](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md) + +## SavedObjectsCreatePointInTimeFinderDependencies.client property + +Signature: + +```typescript +client: Pick; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md new file mode 100644 index 0000000000000..47c640bfabcb0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) + +## SavedObjectsCreatePointInTimeFinderDependencies interface + + +Signature: + +```typescript +export interface SavedObjectsCreatePointInTimeFinderDependencies +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [client](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md) | Pick<SavedObjectsClientContract, 'find' | 'openPointInTimeForType' | 'closePointInTime'> | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderoptions.md new file mode 100644 index 0000000000000..928c6f72bcbf5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderoptions.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreatePointInTimeFinderOptions](./kibana-plugin-core-server.savedobjectscreatepointintimefinderoptions.md) + +## SavedObjectsCreatePointInTimeFinderOptions type + + +Signature: + +```typescript +export declare type SavedObjectsCreatePointInTimeFinderOptions = Omit; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 6f7c05ea469bc..a92b1f48d08eb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -27,10 +27,10 @@ export interface SavedObjectsFindOptions | [preference](./kibana-plugin-core-server.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | | [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | -| [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | unknown[] | Use the sort values from the previous page to retrieve the next page of results. | +| [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | estypes.Id[] | Use the sort values from the previous page to retrieve the next page of results. | | [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | -| [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | string | | +| [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | estypes.SortOrder | | | [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | string | string[] | | | [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md index 6364370948976..9afd602259a78 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md @@ -9,5 +9,5 @@ Use the sort values from the previous page to retrieve the next page of results. Signature: ```typescript -searchAfter?: unknown[]; +searchAfter?: estypes.Id[]; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md index d247b9e38e448..e1c657e3a5171 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md @@ -7,5 +7,5 @@ Signature: ```typescript -sortOrder?: string; +sortOrder?: estypes.SortOrder; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md index 0f8e9c59236bb..a729ce32e1c80 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md @@ -16,5 +16,5 @@ export interface SavedObjectsFindResult extends SavedObject | Property | Type | Description | | --- | --- | --- | | [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. | -| [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | unknown[] | The Elasticsearch sort value of this result. | +| [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | string[] | The Elasticsearch sort value of this result. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md index 17f5268724332..e73d6b4926d89 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md @@ -9,7 +9,7 @@ The Elasticsearch `sort` value of this result. Signature: ```typescript -sort?: unknown[]; +sort?: string[]; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index 68e9bb09456cd..8da2458cf007e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions ``` ## Properties @@ -18,4 +18,5 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt | [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | | [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | | [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | +| [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) | Attributes | Attributes to use when upserting the document if it doesn't exist. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md new file mode 100644 index 0000000000000..d5657dd65771f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) > [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) + +## SavedObjectsIncrementCounterOptions.upsertAttributes property + +Attributes to use when upserting the document if it doesn't exist. + +Signature: + +```typescript +upsertAttributes?: Attributes; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md index 8f9dca35fa362..b9d81c89bffd7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md @@ -6,6 +6,8 @@ Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using `openPointInTimeForType`. +Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md new file mode 100644 index 0000000000000..5d9d2857f6e0b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md @@ -0,0 +1,53 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [createPointInTimeFinder](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) + +## SavedObjectsRepository.createPointInTimeFinder() method + +Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any `find` queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client. + +Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments. + +This generator wraps calls to [SavedObjectsRepository.find()](./kibana-plugin-core-server.savedobjectsrepository.find.md) and iterates over multiple pages of results using `_pit` and `search_after`. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated `perPage`. + +Once you have retrieved all of the results you need, it is recommended to call `close()` to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to `find` fails for any reason. + +Signature: + +```typescript +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| findOptions | SavedObjectsCreatePointInTimeFinderOptions | | +| dependencies | SavedObjectsCreatePointInTimeFinderDependencies | | + +Returns: + +`ISavedObjectsPointInTimeFinder` + +## Example + + +```ts +const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { + type: 'visualization', + search: 'foo*', + perPage: 100, +}; + +const finder = savedObjectsClient.createPointInTimeFinder(findOptions); + +const responses: SavedObjectFindResponse[] = []; +for await (const response of finder.find()) { + responses.push(...response); + if (doneSearching) { + await finder.close(); + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index eb18e064c84e2..59d98bf4d607b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -9,7 +9,7 @@ Increments all the specified counter fields (by one by default). Creates the doc Signature: ```typescript -incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; +incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; ``` ## Parameters @@ -19,7 +19,7 @@ incrementCounter(type: string, id: string, counterFields: Arraystring | The type of saved object whose fields should be incremented | | id | string | The id of the document whose fields should be incremented | | counterFields | Array<string | SavedObjectsIncrementCounterField> | An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) | -| options | SavedObjectsIncrementCounterOptions | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | +| options | SavedObjectsIncrementCounterOptions<T> | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | Returns: @@ -52,5 +52,19 @@ repository 'stats.apiCalls', ]) +// Increment the apiCalls field counter by 4 +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + { fieldName: 'stats.apiCalls' incrementBy: 4 }, + ]) + +// Initialize the document with arbitrary fields if not present +repository.incrementCounter<{ appId: string }>( + 'dashboard_counter_type', + 'counter_id', + [ 'stats.apiCalls'], + { upsertAttributes: { appId: 'myId' } } +) + ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 632d9c279cb88..00e6ed3aeddfc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -20,15 +20,16 @@ export declare class SavedObjectsRepository | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | -| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using openPointInTimeForType. | +| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using openPointInTimeForType.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | +| [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.This generator wraps calls to [SavedObjectsRepository.find()](./kibana-plugin-core-server.savedobjectsrepository.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. | -| [openPointInTimeForType(type, { keepAlive, preference })](./kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to SavedObjects.find to search against that PIT. | +| [openPointInTimeForType(type, { keepAlive, preference })](./kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to SavedObjects.find to search against that PIT.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md index 6b66882484520..b33765bb79dd8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md @@ -6,6 +6,8 @@ Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned `id` can then be passed to `SavedObjects.find` to search against that PIT. +Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md index c8a372edbdb85..073b1d462986c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchresponse.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type IEsSearchResponse = IKibanaSearchResponse>; +export declare type IEsSearchResponse = IKibanaSearchResponse>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md index 5f8966f0227ac..f6421d65bc551 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; +protected handleSearchError(e: KibanaServerError | AbortError, options?: ISearchOptions, isTimeout?: boolean): Error; ``` ## Parameters @@ -15,8 +15,8 @@ protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: Ab | Parameter | Type | Description | | --- | --- | --- | | e | KibanaServerError | AbortError | | -| timeoutSignal | AbortSignal | | | options | ISearchOptions | | +| isTimeout | boolean | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 2247813562dc7..9d18309fc07be 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -27,7 +27,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | | [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | | -| [handleSearchError(e, timeoutSignal, options)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | +| [handleSearchError(e, options, isTimeout)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | | [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md index e96fe8b8e08dc..623d6366d4d13 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md @@ -14,7 +14,7 @@ Fetch this source and reject the returned Promise on error Signature: ```typescript -fetch(options?: ISearchOptions): Promise>; +fetch(options?: ISearchOptions): Promise>; ``` ## Parameters @@ -25,5 +25,5 @@ fetch(options?: ISearchOptions): PromiseReturns: -`Promise>` +`Promise>` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md index bcf220a9a27e6..d5641107a88aa 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md @@ -9,7 +9,7 @@ Fetch this source from Elasticsearch, returning an observable over the response( Signature: ```typescript -fetch$(options?: ISearchOptions): import("rxjs").Observable>; +fetch$(options?: ISearchOptions): import("rxjs").Observable>; ``` ## Parameters @@ -20,5 +20,5 @@ fetch$(options?: ISearchOptions): import("rxjs").ObservableReturns: -`import("rxjs").Observable>` +`import("rxjs").Observable>` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md index d333af1b278c2..be208c0a51c81 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iessearchresponse.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type IEsSearchResponse = IKibanaSearchResponse>; +export declare type IEsSearchResponse = IKibanaSearchResponse>; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md index 698b4bc7f2043..d408f00e33c9e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -14,6 +14,6 @@ export declare class IndexPatternsServiceProvider implements PluginSignature: ```typescript -setup(core: CoreSetup, { logger, expressions }: IndexPatternsServiceSetupDeps): void; +setup(core: CoreSetup, { expressions }: IndexPatternsServiceSetupDeps): void; ``` ## Parameters @@ -15,7 +15,7 @@ setup(core: CoreSetup, { logger, e | Parameter | Type | Description | | --- | --- | --- | | core | CoreSetup<DataPluginStartDependencies, DataPluginStart> | | -| { logger, expressions } | IndexPatternsServiceSetupDeps | | +| { expressions } | IndexPatternsServiceSetupDeps | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 16d9ce457603e..e0734bc017f4f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -109,5 +109,6 @@ | [KibanaContext](./kibana-plugin-plugins-data-server.kibanacontext.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | +| [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md) | | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | 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 f479ffd52e9b8..025cab9f48c1a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md new file mode 100644 index 0000000000000..f031ddfbd09af --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md) + +## SearchRequestHandlerContext type + +Signature: + +```typescript +export declare type SearchRequestHandlerContext = IScopedSearchClient; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md deleted file mode 100644 index dffce4a091718..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [getSavedObject](./kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md) - -## ExecutionContext.getSavedObject property - -Allows to fetch saved objects from ElasticSearch. In browser `getSavedObject` function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. - -Signature: - -```typescript -getSavedObject?: (type: string, id: string) => Promise>; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 901b46f0888d4..1388e04c315e2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -18,7 +18,6 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | -| [getSavedObject](./kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md) | <T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>> | Allows to fetch saved objects from ElasticSearch. In browser getSavedObject function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. | | [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.executioncontext.inspectoradapters.md) | InspectorAdapters | Adapters for inspector plugin. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md deleted file mode 100644 index b8c8f4f3bb067..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [getSavedObject](./kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md) - -## ExecutionContext.getSavedObject property - -Allows to fetch saved objects from ElasticSearch. In browser `getSavedObject` function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. - -Signature: - -```typescript -getSavedObject?: (type: string, id: string) => Promise>; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 39018599a2c92..8503f81ad7d25 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -18,7 +18,6 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | -| [getSavedObject](./kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md) | <T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>> | Allows to fetch saved objects from ElasticSearch. In browser getSavedObject function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. | | [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-server.executioncontext.inspectoradapters.md) | InspectorAdapters | Adapters for inspector plugin. | diff --git a/docs/maps/images/gs_add_cloropeth_layer.png b/docs/maps/images/gs_add_cloropeth_layer.png index 1528f404026f2..42e00ccc5dd24 100644 Binary files a/docs/maps/images/gs_add_cloropeth_layer.png and b/docs/maps/images/gs_add_cloropeth_layer.png differ diff --git a/docs/maps/images/gs_add_es_document_layer.png b/docs/maps/images/gs_add_es_document_layer.png index f4ffbc581745d..d7616c4b11fe0 100644 Binary files a/docs/maps/images/gs_add_es_document_layer.png and b/docs/maps/images/gs_add_es_document_layer.png differ diff --git a/docs/maps/images/sample_data_web_logs.png b/docs/maps/images/sample_data_web_logs.png index 3b0c2ba3f12c0..f4f4de88f1992 100644 Binary files a/docs/maps/images/sample_data_web_logs.png and b/docs/maps/images/sample_data_web_logs.png differ diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index c62aafac00d3f..39ea4daf2ba33 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -67,8 +67,9 @@ and lighter shades will symbolize countries with less traffic. . In **Layer style**, set: -** **Fill color** to the grey color ramp +** **Fill color: As number** to the grey color ramp ** **Border color** to white +** **Label** to symbol label . Click **Save & close**. + @@ -102,7 +103,7 @@ The layer is only visible when users zoom in. . In **Layer settings**, set: ** **Name** to `Actual Requests` -** **Visibilty** to the range [9, 24] +** **Visibility** to the range [9, 24] ** **Opacity** to 100% . Add a tooltip field and select **agent**, **bytes**, **clientip**, **host**, @@ -134,9 +135,9 @@ grids with less bytes transferred. ** **Name** to `Total Requests and Bytes` ** **Visibility** to the range [0, 9] ** **Opacity** to 100% -. Add a metric with: -** **Aggregation** set to **Sum** -** **Field** set to **bytes** +. In **Metrics**, use: +** **Agregation** set to **Count**, and +** **Aggregation** set to **Sum** with **Field** set to **bytes** . In **Layer style**, change **Symbol size**: ** Set the field select to *sum bytes*. ** Set the min size to 7 and the max size to 25 px. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 52d1d63ce0653..f5ebac1ebf02e 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -50,46 +50,56 @@ for example, `logstash-*`. [float] ==== 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. +*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: [source,yaml] ------------------- logging: appenders: - console: - kind: console + custom: + type: console layout: - kind: pattern + type: pattern pattern: "%date{ISO8601_TZ}{UTC}" ------------------- See https://github.com/elastic/kibana/pull/90368 for more details. [float] ==== Responses are never logged by default -*Details:* Previously responses would be logged if either `logging.json` was true, `logging.dest` was specified, or a `TTY` was detected. +*Details:* Previously responses would be logged if either `logging.json` was true, `logging.dest` was specified, or a `TTY` was detected. With the new logging configuration, these are provided by a dedicated logger. -*Impact:* To restore the previous behavior, in kibana.yml enable `debug` logs for the `http.server.response` context under `logging.loggers`: +*Impact:* To restore the previous behavior, in `kibana.yml` enable `debug` for the `http.server.response` logger: [source,yaml] ------------------- logging: + appenders: + custom: + type: console + layout: + type: pattern loggers: - - context: http.server.response - appenders: [console] + - name: http.server.response + appenders: [custom] level: debug ------------------- 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`. +*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. -*Impact:* To restore the previous behavior, in `kibana.yml` use the `console` appender to send logs to `stdout`. +*Impact:* To restore the previous behavior and log records to *stdout*, in `kibana.yml` use an appender with `type: console`. [source,yaml] ------------------- logging: + appenders: + custom: + type: console + layout: + type: pattern root: - appenders: [default, console] + appenders: [default, custom] ------------------- To send logs to `file` with a given file path, you should define a custom appender with `type:file`: @@ -107,16 +117,15 @@ logging: ------------------- [float] -==== Specify log event output with root -*Details:* Previously logging output would be specified by `logging.silent` (none), 'logging.quiet' (error messages only) and `logging.verbose` (all). +==== 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. -*Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level` as one of `off`, `error`, `all`: +*Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level`: [source,yaml] ------------------- # suppress all logs logging: root: - appenders: [default] level: off ------------------- @@ -125,7 +134,6 @@ logging: # only log error messages logging: root: - appenders: [default] level: error ------------------- @@ -134,54 +142,14 @@ logging: # log all events logging: root: - appenders: [default] - level: all -------------------- - -[float] -==== Suppress all log output with root -*Details:* Previously all logging output would be suppressed if `logging.silent` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'off'. -[source,yaml] -------------------- -logging: - root: - appenders: [default] - level: off -------------------- - -[float] -==== Suppress log output with root -*Details:* Previously all logging output other than error messages would be suppressed if `logging.quiet` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'error'. -[source,yaml] -------------------- -logging: - root: - appenders: [default] - level: error -------------------- - -[float] -==== Log all output with root -*Details:* Previously all events would be logged if `logging.verbose` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'all'. -[source,yaml] -------------------- -logging: - root: - appenders: [default] level: all ------------------- [float] -==== Declare log message format for each custom appender -*Details:* Previously all events would be logged in `json` format when `logging.json` was true. +==== Declare log message format +*Details:* Previously all events would be logged in `json` format when `logging.json` was true. With the new logging configuration you can specify the output format with layouts. You can choose between `json` and pattern format depending on your needs. -*Impact:* To restore the previous behavior, in `kibana.yml` configure the logging format for each custom appender with the `appender.layout` property. There is no default for custom appenders and each one must be configured expilictly. +*Impact:* To restore the previous behavior, in `kibana.yml` configure the logging format for each custom appender with the `appender.layout` property. There is no default for custom appenders and each one must be configured expilictly. [source,yaml] ------------------- diff --git a/docs/settings/logging-settings.asciidoc b/docs/settings/logging-settings.asciidoc new file mode 100644 index 0000000000000..aa38d54305eec --- /dev/null +++ b/docs/settings/logging-settings.asciidoc @@ -0,0 +1,173 @@ +[[logging-settings]] +=== Logging settings in {kib} +++++ +Logging settings +++++ + +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context (default) are forwarded to the legacy logging service. +The logging configuration is validated against the predefined schema and if there are +any issues with it, {kib} will fail to start with the detailed error message. + +NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats. +These will be removed when the `default` appender is no longer required. + +Here are some configuration examples for the most common logging use cases: + +[[log-to-file-example]] +==== Log to a file + +Log the default log format to a file instead of to stdout (the default). + +[source,yaml] +---- +logging: + appenders: + file: + type: file + fileName: /var/log/kibana.log + layout: + type: pattern + root: + appenders: [default, file] +---- + +[[log-in-json-ECS-example]] +==== Log in json format + +Log the default log format to json layout instead of pattern (the default). +With `json` layout log messages will be formatted as JSON strings in https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[ECS format] that includes a timestamp, log level, logger, message text and any other metadata that may be associated with the log message itself + +[source,yaml] +---- +logging: + appenders: + json-layout: + type: console + layout: + type: json + root: + appenders: [default, json-layout] +---- + +[[log-with-meta-to-stdout]] +==== Log with meta to stdout + +Include `%meta` in your pattern layout: + +[source,yaml] +---- +logging: + appenders: + console-meta: + type: console + layout: + type: pattern + pattern: "[%date] [%level] [%logger] [%meta] %message" + root: + appenders: [default, console-meta] +---- + +[[log-elasticsearch-queries]] +==== Log {es} queries + +[source,yaml] +-- +logging: + appenders: + console_appender: + type: console + layout: + type: pattern + highlight: true + root: + appenders: [default, console_appender] + level: warn + loggers: + - name: elasticsearch.query + level: debug +-- + +[[change-overall-log-level]] +==== Change overall log level. + +[source,yaml] +---- +logging: + root: + level: debug +---- + +[[customize-specific-log-records]] +==== Customize specific log records +Here is a detailed configuration example that can be used to configure _loggers_, _appenders_ and _layouts_: + +[source,yaml] +---- +logging: + appenders: + console: + type: console + layout: + type: pattern + highlight: true + file: + type: file + fileName: /var/log/kibana.log + custom: + type: console + layout: + type: pattern + pattern: "[%date][%level] %message" + json-file-appender: + type: file + fileName: /var/log/kibana-json.log + layout: + type: json + + root: + appenders: [default, console, file] + level: error + + loggers: + - name: plugins + appenders: [custom] + level: warn + - name: plugins.myPlugin + level: info + - name: server + level: fatal + - name: optimize + appenders: [console] + - name: telemetry + appenders: [json-file-appender] + level: all + - name: metrics.ops + appenders: [console] + level: debug +---- + +Here is what we get with the config above: +[options="header"] +|=== + +| Context name | Appenders | Level + +| root | console, file | error + +| plugins | custom | warn + +| plugins.myPlugin | custom | info + +| server | console, file | fatal + +| optimize | console | error + +| telemetry | json-file-appender | all + +| metrics.ops | console | debug +|=== + +NOTE: If you modify `root.appenders`, make sure to include `default`. + +// For more details about logging configuration, refer to the logging system documentation (update to include a link). diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index f48dbeab9d61a..6483442248cea 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -37,11 +37,6 @@ For more information, see monitoring back-end does not run and {kib} stats are not sent to the monitoring cluster. -a|`monitoring.cluster_alerts.` -`email_notifications.email_address` {ess-icon} - | Specifies the email address where you want to receive cluster alerts. - See <> for details. - | `monitoring.ui.elasticsearch.hosts` | Specifies the location of the {es} cluster where your monitoring data is stored. By default, this is the same as <>. This setting enables diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 62e0f0847cbac..e5cbc2c7ea6db 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -64,11 +64,34 @@ To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. | `elasticsearch.logQueries:` - | *deprecated* This setting is no longer used and will get removed in Kibana 8.0. Instead, set <> to `true` + | *deprecated* This setting is no longer used and will get removed in Kibana 8.0. Instead, configure the `elasticsearch.query` logger. This is useful for seeing the query DSL generated by applications that currently do not have an inspector, for example Timelion and Monitoring. *Default: `false`* +The following example shows a valid `elasticsearch.query` logger configuration: +|=== + +[source,text] +-- +logging: + appenders: + console_appender: + type: console + layout: + type: pattern + highlight: true + root: + appenders: [default, console_appender] + level: warn + loggers: + - name: elasticsearch.query + level: debug +-- + +[cols="2*<"] +|=== + |[[elasticsearch-pingTimeout]] `elasticsearch.pingTimeout:` | Time in milliseconds to wait for {es} to respond to pings. *Default: the value of the <> setting* @@ -249,77 +272,44 @@ To reload the logging settings, send a SIGHUP signal to {kib}. [cols="2*<"] |=== -|[[logging-dest]] `logging.dest:` - | Enables you to specify a file where {kib} stores log output. -*Default: `stdout`* +|[[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. +// TODO: add link to the advanced logging documentation. -| `logging.json:` - | Logs output as JSON. When set to `true`, the logs are formatted as JSON -strings that include timestamp, log level, context, message text, and any other -metadata that may be associated with the log message. -When <> is set, and there is no interactive terminal ("TTY"), -this setting defaults to `true`. *Default: `false`* +|[[logging-root-appenders]] `logging.root.appenders:` +| A list of logging appenders to forward the root level logger instance to. By default `root` is configured with the `default` appender that must be included in the list. This is the configuration that all custom loggers will use unless they're re-configured explicitly. Additional appenders, if configured, can be included in the list. -| `logging.quiet:` - | Set the value of this setting to `true` to suppress all logging output other -than error messages. *Default: `false`* +|[[logging-root-level]] `logging.root.level:` {ess-icon} +| Level at which a log record should be logged. Supported levels are: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. Levels are ordered from _all_ (highest) to _off_ and a log record will be logged it its level is higher than or equal to the level of its logger, otherwise the log record is ignored. Use this value to <>. Set to `all` to log all events, including system usage information and all requests. Set to `off` to silence all logs. *Default: `info`*. -| `logging.rotate:` - | experimental[] Specifies the options for the logging rotate feature. -When not defined, all the sub options defaults would be applied. -The following example shows a valid logging rotate configuration: +|[[logging-loggers]] `logging.loggers:` + | Allows you to <>. -|=== +| `logging.loggers.name:` +| Specific logger instance. -[source,text] --- - logging.rotate: - enabled: true - everyBytes: 10485760 - keepFiles: 10 --- +| `logging.loggers.level:` +| Level at which a log record should be shown. Supported levels are: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. -[cols="2*<"] -|=== +| `logging.loggers.appenders:` +| 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). +// TODO: add link to the advanced logging documentation. + +| `logging.appenders.console:` +| Appender to use for logging records to *stdout*. By default, uses the `[%date][%level][%logger] %message` **pattern** layout. To use a **json**, set the <>. + +| `logging.appenders.file:` +| Allows you to specify a fileName to send log records to on disk. To send <>, add the file appender to `root.appenders`. + +| `logging.appenders.rolling-file:` +| Similar to Log4j's `RollingFileAppender`, this appender will log into a file and rotate if following a rolling strategy when the configured policy triggers. There are currently two policies supported: `size-limit` and `time-interval`. + +The size limit policy will perform a rollover when the log file reaches a maximum `size`. *Default 100mb* -| `logging.rotate.enabled:` - | experimental[] Set the value of this setting to `true` to -enable log rotation. If you do not have a <> set that is different from `stdout` -that feature would not take any effect. *Default: `false`* - -| `logging.rotate.everyBytes:` - | experimental[] The maximum size of a log file (that is `not an exact` limit). After the -limit is reached, a new log file is generated. The default size limit is 10485760 (10 MB) and -this option should be in the range of 1048576 (1 MB) to 1073741824 (1 GB). *Default: `10485760`* - -| `logging.rotate.keepFiles:` - | experimental[] The number of most recent rotated log files to keep -on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` -option has to be in the range of 2 to 1024 files. *Default: `7`* - -| `logging.rotate.pollingInterval:` - | experimental[] The number of milliseconds for the polling strategy in case -the <> is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* - -|[[logging-rotate-usePolling]] `logging.rotate.usePolling:` - | experimental[] By default we try to understand the best way to monitoring -the log file and warning about it. Please be aware there are some systems where watch api is not accurate. In those cases, in order to get the feature working, -the `polling` method could be used enabling that option. *Default: `false`* - -| `logging.silent:` - | Set the value of this setting to `true` to -suppress all logging output. *Default: `false`* - -| `logging.timezone` - | Set to the canonical time zone ID -(for example, `America/Los_Angeles`) to log events using that time zone. -For possible values, refer to -https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. -When not set, log events use the host timezone - -| [[logging-verbose]] `logging.verbose:` {ess-icon} - | Set to `true` to log all events, including system usage information and all -requests. *Default: `false`* +The time interval policy will rotate the log file every given interval of time. *Default 24h* | [[regionmap-ES-map]] `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. @@ -690,6 +680,7 @@ include::{kib-repo-dir}/settings/dev-settings.asciidoc[] include::{kib-repo-dir}/settings/graph-settings.asciidoc[] include::{kib-repo-dir}/settings/fleet-settings.asciidoc[] include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] +include::{kib-repo-dir}/settings/logging-settings.asciidoc[] include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/ml-settings.asciidoc[] diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index cc6e363872808..8603ca9935cac 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -54,6 +54,14 @@ This section highlights common causes of {kib} upgrade failures and how to preve ===== 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. +For example, given the following error message: +> Unable to migrate the corrupt saved object document with _id: 'marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275'. To allow migrations to proceed, please delete this document from the [.kibana_7.12.0_001] index. + +The following steps must be followed to allow the upgrade migration to succeed. +Please be aware the Dashboard having ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` belonging to the space `marketing_space` will no more be available: +1. Delete the corrupt document with `DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275` +2. Restart {kib} + [float] ===== User defined index templates that causes new `.kibana*` indices to have incompatible settings or mappings Matching index templates which specify `settings.refresh_interval` or `mappings` are known to interfere with {kib} upgrades. @@ -120,4 +128,4 @@ In order to rollback after a failed upgrade migration, the saved object indices [[upgrade-migrations-old-indices]] ==== Handling old `.kibana_N` indices -After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file +After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index c41f3d8a829e4..6daf252c524dd 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -99,7 +99,7 @@ include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] include::{kib-repo-dir}/api/session-management.asciidoc[] include::{kib-repo-dir}/api/saved-objects.asciidoc[] -include::{kib-repo-dir}/api/alerts.asciidoc[] +include::{kib-repo-dir}/api/alerting.asciidoc[] include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] diff --git a/docs/user/dashboard/images/lens_advanced_1_1_2.png b/docs/user/dashboard/images/lens_advanced_1_1_2.png index 8b5fe130ce7b7..0247ecf695057 100644 Binary files a/docs/user/dashboard/images/lens_advanced_1_1_2.png and b/docs/user/dashboard/images/lens_advanced_1_1_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_2_1.png b/docs/user/dashboard/images/lens_advanced_2_2_1.png index 3124dd1de0654..3044f1070367d 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_2_1.png and b/docs/user/dashboard/images/lens_advanced_2_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1_1.png b/docs/user/dashboard/images/lens_advanced_3_1_1.png index 4d52a23cc2cff..c3fb697666b46 100644 Binary files a/docs/user/dashboard/images/lens_advanced_3_1_1.png and b/docs/user/dashboard/images/lens_advanced_3_1_1.png differ diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc deleted file mode 100644 index 2945ebc67710c..0000000000000 --- a/docs/user/monitoring/cluster-alerts.asciidoc +++ /dev/null @@ -1,64 +0,0 @@ -[role="xpack"] -[[cluster-alerts]] -= Cluster Alerts - -The *Stack Monitoring > Clusters* page in {kib} summarizes the status of your -{stack}. You can drill down into the metrics to view more information about your -cluster and specific nodes, instances, and indices. - -The Top Cluster Alerts shown on the Clusters page notify you of -conditions that require your attention: - -* {es} Cluster Health Status is Yellow (missing at least one replica) -or Red (missing at least one primary). -* {es} Version Mismatch. You have {es} nodes with -different versions in the same cluster. -* {kib} Version Mismatch. You have {kib} instances with different -versions running against the same {es} cluster. -* Logstash Version Mismatch. You have Logstash nodes with different -versions reporting stats to the same monitoring cluster. -* {es} Nodes Changed. You have {es} nodes that were recently added or removed. -* {es} License Expiration. The cluster's license is about to expire. -+ --- -If you do not preserve the data directory when upgrading a {kib} or -Logstash node, the instance is assigned a new persistent UUID and shows up -as a new instance --- -* {xpack} License Expiration. When the {xpack} license expiration date -approaches, you will get notifications with a severity level relative to how -soon the expiration date is: - ** 60 days: Informational alert - ** 30 days: Low-level alert - ** 15 days: Medium-level alert - ** 7 days: Severe-level alert -+ -The 60-day and 30-day thresholds are skipped for Trial licenses, which are only -valid for 30 days. - -The {monitor-features} check the cluster alert conditions every minute. Cluster -alerts are automatically dismissed when the condition is resolved. - -NOTE: {watcher} must be enabled to view cluster alerts. If you have a Basic -license, Top Cluster Alerts are not displayed. - -[float] -[[cluster-alert-email-notifications]] -== Email Notifications -To receive email notifications for the Cluster Alerts: - -. Configure an email account as described in -{ref}/actions-email.html#configuring-email[Configuring email accounts]. -. Configure the -`monitoring.cluster_alerts.email_notifications.email_address` setting in -`kibana.yml` with your email address. -+ --- -TIP: If you have separate production and monitoring clusters and separate {kib} -instances for those clusters, you must put the -`monitoring.cluster_alerts.email_notifications.email_address` setting in -the {kib} instance that is associated with the production cluster. - --- - -Email notifications are sent only when Cluster Alerts are triggered and resolved. diff --git a/docs/user/monitoring/index.asciidoc b/docs/user/monitoring/index.asciidoc index 514988792d214..e4fd4a8cd085c 100644 --- a/docs/user/monitoring/index.asciidoc +++ b/docs/user/monitoring/index.asciidoc @@ -1,6 +1,5 @@ include::xpack-monitoring.asciidoc[] include::beats-details.asciidoc[leveloffset=+1] -include::cluster-alerts.asciidoc[leveloffset=+1] include::elasticsearch-details.asciidoc[leveloffset=+1] include::kibana-alerts.asciidoc[leveloffset=+1] include::kibana-details.asciidoc[leveloffset=+1] diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 300497126c3e5..04f4e986ca289 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -29,7 +29,7 @@ To review and modify all the available alerts, use This alert is triggered when a node runs a consistently high CPU load. By default, the trigger condition is set at 85% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify internal of 1 day. +checks on a schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-disk-usage-threshold]] @@ -38,7 +38,7 @@ checks on a schedule time of 1 minute with a re-notify internal of 1 day. This alert is triggered when a node is nearly at disk capacity. By default, the trigger condition is set at 80% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify internal of 1 day. +checks on a schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-jvm-memory-threshold]] @@ -47,7 +47,7 @@ checks on a schedule time of 1 minute with a re-notify internal of 1 day. This alert is triggered when a node runs a consistently high JVM memory usage. By default, the trigger condition is set at 85% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify internal of 1 day. +checks on a schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-missing-monitoring-data]] @@ -56,7 +56,72 @@ checks on a schedule time of 1 minute with a re-notify internal of 1 day. This alert is triggered when any stack products nodes or instances stop sending monitoring data. By default, the trigger condition is set to missing for 15 minutes looking back 1 day. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify internal of 6 hours. +checks on a schedule time of 1 minute with a re-notify interval of 6 hours. + +[discrete] +[[kibana-alerts-thread-pool-rejections]] +== Thread pool rejections (search/write) + +This alert is triggered when a node experiences thread pool rejections. By +default, the trigger condition is set at 300 or more over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify interval of 1 day. +Thresholds can be set independently for `search` and `write` type rejections. + +[discrete] +[[kibana-alerts-ccr-read-exceptions]] +== CCR read exceptions + +This alert is triggered if a read exception has been detected on any of the +replicated clusters. The trigger condition is met if 1 or more read exceptions +are detected in the last hour. The alert is grouped across all replicated clusters +by running checks on a schedule time of 1 minute with a re-notify interval of 6 hours. + +[discrete] +[[kibana-alerts-large-shard-size]] +== Large shard size + +This alert is triggered if a large (primary) shard size is found on any of the +specified index patterns. The trigger condition is met if an index's shard size is +55gb or higher in the last 5 minutes. The alert is grouped across all indices that match +the default patter of `*` by running checks on a schedule time of 1 minute with a re-notify +interval of 12 hours. + +[discrete] +[[kibana-alerts-cluster-alerts]] +== Cluster alerts + +These alerts summarize the current status of your {stack}. You can drill down into the metrics +to view more information about your cluster and specific nodes, instances, and indices. + +An alert will be triggered if any of the following conditions are met within the last minute: + +* {es} cluster health status is yellow (missing at least one replica) +or red (missing at least one primary). +* {es} version mismatch. You have {es} nodes with +different versions in the same cluster. +* {kib} version mismatch. You have {kib} instances with different +versions running against the same {es} cluster. +* Logstash version mismatch. You have Logstash nodes with different +versions reporting stats to the same monitoring cluster. +* {es} nodes changed. You have {es} nodes that were recently added or removed. +* {es} license expiration. The cluster's license is about to expire. ++ +-- +If you do not preserve the data directory when upgrading a {kib} or +Logstash node, the instance is assigned a new persistent UUID and shows up +as a new instance +-- +* Subscription license expiration. When the expiration date +approaches, you will get notifications with a severity level relative to how +soon the expiration date is: + ** 60 days: Informational alert + ** 30 days: Low-level alert + ** 15 days: Medium-level alert + ** 7 days: Severe-level alert ++ +The 60-day and 30-day thresholds are skipped for Trial licenses, which are only +valid for 30 days. NOTE: Some action types are subscription features, while others are free. For a comparison of the Elastic subscription levels, see the alerting section of diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index 57c255c809dc5..6294a4fe6f14a 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -49,3 +49,16 @@ It is difficult to predict how much throughput is needed to ensure all rules and By counting rules as recurring tasks and actions as non-recurring tasks, a rough throughput <> as a _tasks per minute_ measurement. Predicting the buffer required to account for actions depends heavily on the rule types you use, the amount of alerts they might detect, and the number of actions you might choose to assign to action groups. With that in mind, regularly <> of your Task Manager instances. + +[float] +[[event-log-ilm]] +=== Event log index lifecycle managment + +Alerts and actions log activity in a set of "event log" indices. These indices are configured with an index lifecycle management (ILM) policy, which you can customize. The default policy rolls over the index when it reaches 50GB, or after 30 days. Indices over 90 days old are deleted. + +The name of the index policy is `kibana-event-log-policy`. {kib} creates the index policy on startup, if it doesn't already exist. The index policy can be customized for your environment, but {kib} never modifies the index policy after creating it. + +Because Kibana uses the documents to display historic data, you should set the delete phase longer than you would like the historic data to be shown. For example, if you would like to see one month's worth of historic data, you should set the delete phase to at least one month. + +For more information on index lifecycle management, see: +{ref}/index-lifecycle-management.html[Index Lifecycle Policies]. diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc index c96b294c0c50d..5e75aef0d9570 100644 --- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc +++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc @@ -706,3 +706,21 @@ These rough calculations give you a lower bound to the required throughput, whic Given these inferred attributes, it would be safe to assume that a single {kib} instance with default settings **would not** provide the required throughput. It is possible that scaling horizontally by adding a couple more {kib} instances will. For details on scaling Task Manager, see <>. + +[float] +[[task-manager-cannot-operate-when-inline-scripts-are-disabled]] +==== Inline scripts are disabled in {es} + +*Problem*: + +Tasks are not running, and the server logs contain the following error message: + +[source, txt] +-------------------------------------------------- +[warning][plugins][taskManager] Task Manager cannot operate when inline scripts are disabled in {es} +-------------------------------------------------- + +*Solution*: + +Inline scripts are a hard requirement for Task Manager to function. +To enable inline scripting, see the Elasticsearch documentation for {ref}/modules-scripting-security.html#allowed-script-types-setting[configuring allowed script types setting]. diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index ebe095e0881b3..c43e9210dd7c8 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -126,10 +126,10 @@ all, the full logs from Reporting will be the first place to look. In `kibana.ym [source,yaml] -------------------------------------------------------------------------------- -logging.verbose: true +logging.root.level: all -------------------------------------------------------------------------------- -For more information about logging, see <>. +For more information about logging, see <>. [float] [[reporting-troubleshooting-puppeteer-debug-logs]] diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx index 8822be035a3d1..c87bf21e0e71c 100644 --- a/examples/search_examples/public/search/app.tsx +++ b/examples/search_examples/public/search/app.tsx @@ -145,7 +145,8 @@ export const SearchExamplesApp = ({ setResponse(res.rawResponse); setTimeTook(res.rawResponse.took); const avgResult: number | undefined = res.rawResponse.aggregations - ? res.rawResponse.aggregations[1].value + ? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + res.rawResponse.aggregations[1].value : undefined; const message = ( diff --git a/examples/search_examples/public/search_sessions/app.tsx b/examples/search_examples/public/search_sessions/app.tsx index bf57964dc1f86..a768600db24ee 100644 --- a/examples/search_examples/public/search_sessions/app.tsx +++ b/examples/search_examples/public/search_sessions/app.tsx @@ -702,13 +702,15 @@ function doSearch( const startTs = performance.now(); // Submit the search request using the `data.search` service. + // @ts-expect-error request.params is incompatible. Filter is not assignable to QueryContainer return data.search .search(req, { sessionId }) .pipe( tap((res) => { if (isCompleteResponse(res)) { const avgResult: number | undefined = res.rawResponse.aggregations - ? res.rawResponse.aggregations[1]?.value ?? res.rawResponse.aggregations[2]?.value + ? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + res.rawResponse.aggregations[1]?.value ?? res.rawResponse.aggregations[2]?.value : undefined; const message = ( diff --git a/package.json b/package.json index 7cb6a505eeafe..e379123269847 100644 --- a/package.json +++ b/package.json @@ -95,18 +95,23 @@ "yarn": "^1.21.1" }, "dependencies": { + "@elastic/apm-rum": "^5.6.1", + "@elastic/apm-rum-react": "^1.2.5", + "@elastic/charts": "26.0.0", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.3", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", - "@elastic/eui": "31.7.0", + "@elastic/eui": "31.10.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", + "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@elastic/react-search-ui": "^1.5.1", "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "link:packages/elastic-safer-lodash-set", "@elastic/search-ui-app-search-connector": "^1.5.0", + "@elastic/ui-ace": "0.2.3", "@hapi/boom": "^9.1.1", "@hapi/cookie": "^11.0.2", "@hapi/good-squeeze": "6.0.0", @@ -122,18 +127,26 @@ "@kbn/apm-utils": "link:packages/kbn-apm-utils", "@kbn/config": "link:packages/kbn-config", "@kbn/config-schema": "link:packages/kbn-config-schema", + "@kbn/crypto": "link:packages/kbn-crypto", "@kbn/i18n": "link:packages/kbn-i18n", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/legacy-logging": "link:packages/kbn-legacy-logging", "@kbn/logging": "link:packages/kbn-logging", "@kbn/monaco": "link:packages/kbn-monaco", + "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/std": "link:packages/kbn-std", "@kbn/tinymath": "link:packages/kbn-tinymath", "@kbn/ui-framework": "link:packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:packages/kbn-ui-shared-deps", + "@kbn/utility-types": "link:packages/kbn-utility-types", "@kbn/utils": "link:packages/kbn-utils", "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", + "@mapbox/geojson-rewind": "^0.5.0", + "@mapbox/mapbox-gl-draw": "^1.2.0", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", + "@mapbox/vector-tile": "1.3.1", + "@scant/router": "^0.1.1", "@slack/webhook": "^5.0.4", "@turf/along": "6.0.1", "@turf/area": "6.0.1", @@ -151,41 +164,60 @@ "accept": "3.0.2", "ajv": "^6.12.4", "angular": "^1.8.0", + "angular-aria": "^1.8.0", "angular-elastic": "^2.5.1", + "angular-recursion": "^1.0.5", "angular-resource": "1.8.0", + "angular-route": "^1.8.0", "angular-sanitize": "^1.8.0", + "angular-sortable-view": "^0.0.17", "angular-ui-ace": "0.2.3", "antlr4ts": "^0.5.0-alpha.3", "apollo-cache-inmemory": "1.6.2", "apollo-client": "^2.3.8", + "apollo-link": "^1.2.3", + "apollo-link-error": "^1.1.7", "apollo-link-http": "^1.5.16", "apollo-link-http-common": "^0.2.15", "apollo-link-schema": "^1.1.0", + "apollo-link-state": "^0.4.1", "apollo-server-core": "^1.3.6", "apollo-server-errors": "^2.0.2", "apollo-server-hapi": "^1.3.6", "archiver": "^5.2.0", "axios": "^0.21.1", + "base64-js": "^1.3.1", "bluebird": "3.5.5", "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "chalk": "^4.1.0", "check-disk-space": "^2.1.0", + "cheerio": "0.22.0", "chokidar": "^3.4.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "color": "1.0.3", "commander": "^3.0.2", + "compare-versions": "3.5.1", "concat-stream": "1.6.2", + "constate": "^1.3.2", + "cronstrue": "^1.51.0", "content-disposition": "0.5.3", + "copy-to-clipboard": "^3.0.8", "core-js": "^3.6.5", + "css-minimizer-webpack-plugin": "^1.3.0", "custom-event-polyfill": "^0.3.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", + "d3": "3.5.17", "d3-array": "1.2.4", + "d3-cloud": "1.2.5", + "d3-scale": "1.0.7", "d3-shape": "^1.1.0", "d3-time": "^1.1.0", "dedent": "^0.7.0", "deep-freeze-strict": "^1.1.1", + "deepmerge": "^4.2.2", "del": "^5.1.0", "elastic-apm-node": "^3.10.0", "elasticsearch": "^16.7.0", @@ -194,9 +226,11 @@ "expiry-js": "0.1.7", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.1", + "file-saver": "^1.3.8", "file-type": "^10.9.0", "focus-trap-react": "^3.1.1", "font-awesome": "4.7.0", + "formsy-react": "^1.1.5", "fp-ts": "^2.3.1", "geojson-vt": "^3.2.1", "get-port": "^5.0.0", @@ -212,31 +246,51 @@ "graphql-tag": "^2.10.3", "graphql-tools": "^3.0.2", "handlebars": "4.7.7", + "he": "^1.2.0", "history": "^4.9.0", + "history-extra": "^5.0.1", "hjson": "3.2.1", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", + "i18n-iso-countries": "^4.3.1", + "icalendar": "0.7.1", "idx": "^2.5.6", "immer": "^8.0.1", "inline-style": "^2.0.0", "intl": "^1.2.5", "intl-format-cache": "^2.1.0", "intl-messageformat": "^2.2.0", + "intl-messageformat-parser": "^1.4.0", "intl-relativeformat": "^2.1.0", "io-ts": "^2.0.5", "ipaddr.js": "2.0.0", "isbinaryfile": "4.0.2", "joi": "^13.5.2", "jquery": "^3.5.0", + "js-levenshtein": "^1.1.6", + "js-search": "^1.4.3", "js-yaml": "^3.14.0", "json-stable-stringify": "^1.0.1", + "json-stringify-pretty-compact": "1.2.0", "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", + "jsts": "^1.6.2", + "kea": "^2.3.0", + "leaflet": "1.5.1", + "leaflet-draw": "0.4.14", + "leaflet-responsive-popup": "0.6.4", + "leaflet.heat": "0.2.0", + "less": "npm:@elastic/less@2.7.3-kibana", "load-json-file": "^6.2.0", + "loader-utils": "^1.2.3", "lodash": "^4.17.21", "lru-cache": "^4.1.5", + "lz-string": "^1.4.4", "markdown-it": "^10.0.0", + "mapbox-gl": "1.13.1", + "mapbox-gl-draw-rectangle-mode": "^1.0.4", "md5": "^2.1.0", + "memoize-one": "^5.0.0", "mime": "^2.4.4", "mime-types": "^2.1.27", "mini-css-extract-plugin": "0.8.0", @@ -261,38 +315,69 @@ "papaparse": "^5.2.0", "pdfmake": "^0.1.65", "pegjs": "0.10.0", + "p-limit": "^3.0.1", + "pluralize": "3.1.0", "pngjs": "^3.4.0", + "polished": "^1.9.2", "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "proxy-from-env": "1.0.0", + "proxyquire": "1.8.0", "puid": "1.0.7", "puppeteer": "npm:@elastic/puppeteer@5.4.1-patch.1", "query-string": "^6.13.2", "raw-loader": "^3.1.0", + "rbush": "^3.0.1", + "re-resizable": "^6.1.1", "re2": "^1.15.4", "react": "^16.12.0", "react-ace": "^5.9.0", + "react-apollo": "^2.1.4", + "react-beautiful-dnd": "^13.0.0", "react-color": "^2.13.8", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", + "react-dropzone": "^4.2.9", + "react-fast-compare": "^2.0.4", + "react-grid-layout": "^0.16.2", "react-input-range": "^1.3.0", "react-intl": "^2.8.0", "react-is": "^16.8.0", + "react-markdown": "^4.3.1", "react-moment-proptypes": "^1.7.0", + "react-monaco-editor": "^0.41.2", + "react-popper-tooltip": "^2.10.1", "react-query": "^3.12.0", + "react-resize-detector": "^4.2.0", + "react-reverse-portal": "^1.0.4", + "react-router-redux": "^4.0.8", + "react-shortcuts": "^2.0.0", + "react-sizeme": "^2.3.6", + "react-syntax-highlighter": "^15.3.1", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", + "react-tiny-virtual-list": "^2.2.0", + "react-virtualized": "^9.21.2", "react-use": "^15.3.8", + "react-vis": "^1.8.1", + "react-visibility-sensor": "^5.1.1", + "reactcss": "1.2.3", "recompose": "^0.26.0", + "reduce-reducers": "^1.0.4", "redux": "^4.0.5", "redux-actions": "^2.6.5", + "redux-devtools-extension": "^2.13.8", "redux-observable": "^1.2.0", + "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", + "redux-thunks": "^1.0.0", "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "require-in-the-middle": "^5.0.2", + "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.0", "rison-node": "1.0.2", "rxjs": "^6.5.5", "seedrandom": "^3.0.5", @@ -305,17 +390,30 @@ "style-it": "^2.1.3", "styled-components": "^5.1.0", "symbol-observable": "^1.2.0", + "suricata-sid-db": "^1.0.2", "tabbable": "1.1.3", "tar": "4.4.13", + "tinycolor2": "1.4.1", "tinygradient": "0.4.3", + "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "ts-easing": "^0.2.0", "tslib": "^2.0.0", "type-detect": "^4.0.8", + "typescript-fsa": "^3.0.0", + "typescript-fsa-reducers": "^1.2.2", "ui-select": "0.19.8", "unified": "^9.2.1", + "unstated": "^2.1.1", + "use-resize-observer": "^6.0.0", "utility-types": "^3.10.0", "uuid": "3.3.2", + "vega": "^5.19.1", + "vega-lite": "^5.0.0", + "vega-schema-url-parser": "^2.1.0", + "vega-spec-injector": "^0.0.2", + "vega-tooltip": "^0.25.1", + "venn.js": "0.2.20", "vinyl": "^2.2.0", "vt-pbf": "^3.1.1", "wellknown": "^0.5.0", @@ -347,17 +445,15 @@ "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "25.3.0", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", - "@elastic/maki": "6.3.0", - "@elastic/ui-ace": "0.2.3", "@istanbuljs/schema": "^0.1.2", "@jest/reporters": "^26.5.2", "@kbn/babel-code-parser": "link:packages/kbn-babel-code-parser", "@kbn/babel-preset": "link:packages/kbn-babel-preset", + "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", "@kbn/dev-utils": "link:packages/kbn-dev-utils", "@kbn/docs-utils": "link:packages/kbn-docs-utils", "@kbn/es": "link:packages/kbn-es", @@ -373,17 +469,11 @@ "@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", - "@kbn/utility-types": "link:packages/kbn-utility-types", "@loaders.gl/polyfills": "^2.3.5", - "@mapbox/geojson-rewind": "^0.5.0", - "@mapbox/mapbox-gl-draw": "^1.2.0", - "@mapbox/mapbox-gl-rtl-text": "^0.2.3", - "@mapbox/vector-tile": "1.3.1", "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@octokit/rest": "^16.35.0", "@percy/agent": "^0.28.6", - "@scant/router": "^0.1.0", "@storybook/addon-a11y": "^6.1.20", "@storybook/addon-actions": "^6.1.20", "@storybook/addon-docs": "^6.1.20", @@ -456,7 +546,6 @@ "@types/he": "^1.1.1", "@types/history": "^4.7.3", "@types/hjson": "^2.4.2", - "@types/hoist-non-react-statics": "^3.3.1", "@types/http-proxy": "^1.17.4", "@types/http-proxy-agent": "^2.0.2", "@types/inquirer": "^7.3.1", @@ -476,7 +565,6 @@ "@types/listr": "^0.14.0", "@types/loader-utils": "^1.1.3", "@types/lodash": "^4.14.159", - "@types/log-symbols": "^2.0.0", "@types/lru-cache": "^5.1.0", "@types/mapbox-gl": "^1.9.1", "@types/markdown-it": "^0.0.7", @@ -563,21 +651,13 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^4.14.1", "@typescript-eslint/parser": "^4.14.1", - "@welldone-software/why-did-you-render": "^5.0.0", "@yarnpkg/lockfile": "^1.1.0", "abab": "^2.0.4", "aggregate-error": "^3.1.0", - "angular-aria": "^1.8.0", "angular-mocks": "^1.7.9", - "angular-recursion": "^1.0.5", - "angular-route": "^1.8.0", - "angular-sortable-view": "^0.0.17", "antlr4ts-cli": "^0.5.0-alpha.3", "apidoc": "^0.25.0", "apidoc-markdown": "^5.1.8", - "apollo-link": "^1.2.3", - "apollo-link-error": "^1.1.7", - "apollo-link-state": "^0.4.1", "argsplit": "^1.0.5", "autoprefixer": "^9.7.4", "axe-core": "^4.0.2", @@ -590,34 +670,23 @@ "babel-plugin-styled-components": "^1.10.7", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "backport": "^5.6.6", - "base64-js": "^1.3.1", "base64url": "^3.0.1", - "broadcast-channel": "^3.0.3", "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "cheerio": "0.22.0", "chromedriver": "^89.0.0", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", - "compare-versions": "3.5.1", "compression-webpack-plugin": "^4.0.0", - "constate": "^1.3.2", - "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", - "cronstrue": "^1.51.0", "css-loader": "^3.4.2", "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "cypress-pipe": "^2.0.0", "cypress-promise": "^1.1.0", - "d3": "3.5.17", - "d3-cloud": "1.2.5", - "d3-scale": "1.0.7", "debug": "^2.6.9", - "deepmerge": "^4.2.2", "del-cli": "^3.0.1", "delete-empty": "^2.0.0", "dependency-check": "^4.1.0", @@ -654,9 +723,7 @@ "fast-glob": "2.2.7", "fetch-mock": "^7.3.9", "file-loader": "^4.2.0", - "file-saver": "^1.3.8", "form-data": "^4.0.0", - "formsy-react": "^1.1.5", "geckodriver": "^1.22.2", "glob-watcher": "5.0.3", "graphql-code-generator": "^0.18.2", @@ -675,19 +742,10 @@ "gulp-zip": "^5.0.2", "has-ansi": "^3.0.0", "hdr-histogram-js": "^1.2.0", - "he": "^1.2.0", - "highlight.js": "^9.18.5", - "history-extra": "^5.0.1", - "hoist-non-react-statics": "^3.3.2", "html": "1.0.0", "html-loader": "^0.5.5", "http-proxy": "^1.18.1", - "i18n-iso-countries": "^4.3.1", - "icalendar": "0.7.1", - "iedriver": "^3.14.2", - "imports-loader": "^0.8.0", "inquirer": "^7.3.3", - "intl-messageformat-parser": "^1.4.0", "is-glob": "^4.0.1", "is-path-inside": "^3.0.2", "istanbul-instrumenter-loader": "^3.0.1", @@ -705,31 +763,14 @@ "jest-styled-components": "^7.0.2", "jest-when": "^2.7.2", "jimp": "^0.14.0", - "js-levenshtein": "^1.1.6", - "js-search": "^1.4.3", "jsdom": "13.1.0", - "json-stringify-pretty-compact": "1.2.0", "json5": "^1.0.1", "jsondiffpatch": "0.4.1", - "jsts": "^1.6.2", - "kea": "^2.3.0", - "keymirror": "0.1.1", - "leaflet": "1.5.1", - "leaflet-draw": "0.4.14", - "leaflet-responsive-popup": "0.6.4", - "leaflet.heat": "0.2.0", - "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", "lmdb-store": "^0.9.0", "load-grunt-config": "^3.0.1", - "loader-utils": "^1.2.3", - "log-symbols": "^2.2.0", - "lz-string": "^1.4.4", - "mapbox-gl": "1.13.1", - "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", - "memoize-one": "^5.0.0", "micromatch": "3.1.10", "minimist": "^1.2.5", "mkdirp": "0.5.1", @@ -741,8 +782,6 @@ "mock-http-server": "1.3.0", "ms-chromium-edge-driver": "^0.2.3", "multimatch": "^4.0.0", - "multistream": "^2.1.1", - "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", "ncp": "^2.0.0", "node-sass": "^4.14.1", @@ -750,53 +789,19 @@ "nyc": "^15.0.1", "oboe": "^2.1.4", "ora": "^4.0.4", - "p-limit": "^3.0.1", "parse-link-header": "^1.0.1", "pbf": "3.2.1", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", - "pkg-up": "^2.0.0", - "pluralize": "3.1.0", - "polished": "^1.9.2", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "postcss-prefix-selector": "^1.7.2", "prettier": "^2.2.0", "pretty-ms": "5.0.0", - "proxyquire": "1.8.0", "q": "^1.5.1", - "querystring": "^0.2.0", - "rbush": "^3.0.1", - "re-resizable": "^6.1.1", - "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^13.0.0", - "react-docgen-typescript-loader": "^3.1.1", - "react-dropzone": "^4.2.9", - "react-fast-compare": "^2.0.4", - "react-grid-layout": "^0.16.2", - "react-markdown": "^4.3.1", - "react-monaco-editor": "^0.41.2", - "react-popper-tooltip": "^2.10.1", - "react-resize-detector": "^4.2.0", - "react-reverse-portal": "^1.0.4", - "react-router-redux": "^4.0.8", - "react-shortcuts": "^2.0.0", - "react-sizeme": "^2.3.6", - "react-syntax-highlighter": "^15.3.1", "react-test-renderer": "^16.12.0", - "react-tiny-virtual-list": "^2.2.0", - "react-virtualized": "^9.21.2", - "react-vis": "^1.8.1", - "react-visibility-sensor": "^5.1.1", - "reactcss": "1.2.3", "read-pkg": "^5.2.0", - "reduce-reducers": "^1.0.4", - "redux-devtools-extension": "^2.13.8", - "redux-saga": "^1.1.3", - "redux-thunks": "^1.0.0", "regenerate": "^1.4.0", - "reselect": "^4.0.0", - "resize-observer-polyfill": "^1.5.0", "resolve": "^1.7.1", "rxjs-marbles": "^5.0.6", "sass-loader": "^8.0.2", @@ -816,31 +821,18 @@ "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", "supports-color": "^7.0.0", - "suricata-sid-db": "^1.0.2", "tape": "^5.0.1", "tar-fs": "^2.1.0", "tempy": "^0.3.0", "terminal-link": "^2.1.1", "terser-webpack-plugin": "^2.1.2", - "tinycolor2": "1.4.1", - "topojson-client": "3.0.0", "ts-loader": "^7.0.5", "ts-morph": "^9.1.0", "tsd": "^0.13.1", "typescript": "4.1.3", - "typescript-fsa": "^3.0.0", - "typescript-fsa-reducers": "^1.2.2", "unlazy-loader": "^0.1.3", - "unstated": "^2.1.1", "url-loader": "^2.2.0", - "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.19.1", - "vega-lite": "^4.17.0", - "vega-schema-url-parser": "^2.1.0", - "vega-spec-injector": "^0.0.2", - "vega-tooltip": "^0.25.0", - "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "wait-on": "^5.2.1", "watchpack": "^1.6.0", diff --git a/src/dev/cli_dev_mode/README.md b/packages/kbn-cli-dev-mode/README.md similarity index 72% rename from src/dev/cli_dev_mode/README.md rename to packages/kbn-cli-dev-mode/README.md index 397017027a52f..6ce41249674ce 100644 --- a/src/dev/cli_dev_mode/README.md +++ b/packages/kbn-cli-dev-mode/README.md @@ -26,8 +26,12 @@ The `DevServer` object is responsible for everything related to running and rest The `Optimizer` object manages a `@kbn/optimizer` instance, adapting its configuration and logging to the data available to the CLI. -## `BasePathProxyServer` (currently passed from core) +## `BasePathProxyServer` -The `BasePathProxyServer` is passed to the `CliDevMode` from core when the dev mode is trigged by the `--dev` flag. This proxy injects a random three character base path in the URL that Kibana is served from to help ensure that Kibana features are written to adapt to custom base path configurations from users. +This proxy injects a random three character base path in the URL that Kibana is served from to help ensure that Kibana features +are written to adapt to custom base path configurations from users. -The basePathProxy also has another important job, ensuring that requests don't fail because the server is restarting and that the browser receives front-end assets containing all saved changes. We accomplish this by observing the ready state of the `Optimizer` and `DevServer` objects and pausing all requests through the proxy until both objects report that they aren't building/restarting based on recently saved changes. \ No newline at end of file +The basePathProxy also has another important job, ensuring that requests don't fail because the server is restarting and +that the browser receives front-end assets containing all saved changes. We accomplish this by observing the ready state of +the `Optimizer` and `DevServer` objects and pausing all requests through the proxy until both objects report that +they aren't building/restarting based on recently saved changes. \ No newline at end of file diff --git a/packages/kbn-cli-dev-mode/jest.config.js b/packages/kbn-cli-dev-mode/jest.config.js new file mode 100644 index 0000000000000..d04dc571ef2a0 --- /dev/null +++ b/packages/kbn-cli-dev-mode/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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: ['/packages/kbn-cli-dev-mode'], +}; diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json new file mode 100644 index 0000000000000..2ee9831e96084 --- /dev/null +++ b/packages/kbn-cli-dev-mode/package.json @@ -0,0 +1,26 @@ +{ + "name": "@kbn/cli-dev-mode", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "kibana": { + "devOnly": true + }, + "dependencies": { + "@kbn/config": "link:../kbn-config", + "@kbn/config-schema": "link:../kbn-config-schema", + "@kbn/logging": "link:../kbn-logging", + "@kbn/server-http-tools": "link:../kbn-server-http-tools", + "@kbn/optimizer": "link:../kbn-optimizer", + "@kbn/std": "link:../kbn-std", + "@kbn/dev-utils": "link:../kbn-dev-utils", + "@kbn/utils": "link:../kbn-utils" + } +} \ No newline at end of file diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.test.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.test.ts new file mode 100644 index 0000000000000..c99485c273364 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.test.ts @@ -0,0 +1,358 @@ +/* + * Copyright 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 { EMPTY } from 'rxjs'; +import supertest from 'supertest'; +import { + getServerOptions, + getListenerOptions, + createServer, + IHttpConfig, +} from '@kbn/server-http-tools'; +import { ByteSizeValue } from '@kbn/config-schema'; + +import { BasePathProxyServer, BasePathProxyServerOptions } from './base_path_proxy_server'; +import { DevConfig } from './config/dev_config'; +import { TestLog } from './log'; + +describe('BasePathProxyServer', () => { + let server: Server; + let proxyServer: BasePathProxyServer; + let logger: TestLog; + let config: IHttpConfig; + let basePath: string; + let proxySupertest: supertest.SuperTest; + + beforeEach(async () => { + logger = new TestLog(); + + config = { + host: '127.0.0.1', + port: 10012, + keepaliveTimeout: 1000, + socketTimeout: 1000, + cors: { + enabled: false, + allowCredentials: false, + allowOrigin: [], + }, + ssl: { enabled: false }, + maxPayload: new ByteSizeValue(1024), + }; + + const serverOptions = getServerOptions(config); + const listenerOptions = getListenerOptions(config); + server = createServer(serverOptions, listenerOptions); + + // setup and start the proxy server + const proxyConfig: IHttpConfig = { ...config, port: 10013 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServer = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: BasePathProxyServerOptions = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServer.start(options); + + // set the base path or throw if for some unknown reason it is not setup + if (proxyServer.basePath == null) { + throw new Error('Invalid null base path, all tests will fail'); + } else { + basePath = proxyServer.basePath; + } + proxySupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await server.stop(); + await proxyServer.stop(); + jest.clearAllMocks(); + }); + + test('root URL will return a 302 redirect', async () => { + await proxySupertest.get('/').expect(302); + }); + + test('root URL will return a redirect location with exactly 3 characters that are a-z', async () => { + const res = await proxySupertest.get('/'); + const location = res.header.location; + expect(location).toMatch(/[a-z]{3}/); + }); + + test('forwards request with the correct path', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/{test}`, + handler: (request, h) => { + return h.response(request.params.test); + }, + }); + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .expect(200) + .then((res) => { + expect(res.text).toBe('some-string'); + }); + }); + + test('forwards request with the correct query params', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response(request.query); + }, + }); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/?bar=test&quux=123`) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', quux: '123' }); + }); + }); + + test('forwards the request body', async () => { + server.route({ + method: 'POST', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response(request.payload); + }, + }); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('returns the correct status code', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response({ foo: 'bar' }).code(417); + }, + }); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/`) + .expect(417) + .then((res) => { + expect(res.body).toEqual({ foo: 'bar' }); + }); + }); + + test('returns the response headers', async () => { + server.route({ + method: 'GET', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response({ foo: 'bar' }).header('foo', 'bar'); + }, + }); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/`) + .expect(200) + .then((res) => { + expect(res.get('foo')).toEqual('bar'); + }); + }); + + test('handles putting', async () => { + server.route({ + method: 'PUT', + path: `${basePath}/foo/`, + handler: (request, h) => { + return h.response(request.payload); + }, + }); + + await server.start(); + + await proxySupertest + .put(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('handles deleting', async () => { + server.route({ + method: 'DELETE', + path: `${basePath}/foo/{test}`, + handler: (request, h) => { + return h.response(request.params.test); + }, + }); + await server.start(); + + await proxySupertest + .delete(`${basePath}/foo/some-string`) + .expect(200) + .then((res) => { + expect(res.text).toBe('some-string'); + }); + }); + + describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { + beforeEach(async () => { + const configWithBasePath: IHttpConfig = { + ...config, + basePath: '/bar', + rewriteBasePath: false, + } as IHttpConfig; + + const serverOptions = getServerOptions(configWithBasePath); + const listenerOptions = getListenerOptions(configWithBasePath); + server = createServer(serverOptions, listenerOptions); + + server.route({ + method: 'GET', + path: `${basePath}/`, + handler: (request, h) => { + return h.response('value:/'); + }, + }); + server.route({ + method: 'GET', + path: `${basePath}/foo`, + handler: (request, h) => { + return h.response('value:/foo'); + }, + }); + + await server.start(); + }); + + test('/bar => 404', async () => { + await proxySupertest.get(`${basePath}/bar`).expect(404); + }); + + test('/bar/ => 404', async () => { + await proxySupertest.get(`${basePath}/bar/`).expect(404); + }); + + test('/bar/foo => 404', async () => { + await proxySupertest.get(`${basePath}/bar/foo`).expect(404); + }); + + test('/ => /', async () => { + await proxySupertest + .get(`${basePath}/`) + .expect(200) + .then((res) => { + expect(res.text).toBe('value:/'); + }); + }); + + test('/foo => /foo', async () => { + await proxySupertest + .get(`${basePath}/foo`) + .expect(200) + .then((res) => { + expect(res.text).toBe('value:/foo'); + }); + }); + }); + + describe('shouldRedirect', () => { + let proxyServerWithoutShouldRedirect: BasePathProxyServer; + let proxyWithoutShouldRedirectSupertest: supertest.SuperTest; + + beforeEach(async () => { + // setup and start a proxy server which does not use "shouldRedirectFromOldBasePath" + const proxyConfig: IHttpConfig = { ...config, port: 10004 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServerWithoutShouldRedirect = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => false, // Return false to not redirect + delayUntil: () => EMPTY, + }; + await proxyServerWithoutShouldRedirect.start(options); + proxyWithoutShouldRedirectSupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await proxyServerWithoutShouldRedirect.stop(); + }); + + test('it will do a redirect if it detects what looks like a stale or previously used base path', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + const res = await proxySupertest.get(`/${fakeBasePath}`).expect(302); + const location = res.header.location; + expect(location).toEqual(`${basePath}/`); + }); + + test('it will NOT do a redirect if it detects what looks like a stale or previously used base path if we intentionally turn it off', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + await proxyWithoutShouldRedirectSupertest.get(`/${fakeBasePath}`).expect(404); + }); + + test('it will NOT redirect if it detects a larger path than 3 characters', async () => { + await proxySupertest.get('/abcde').expect(404); + }); + + test('it will NOT redirect if it is not a GET verb', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + await proxySupertest.put(`/${fakeBasePath}`).expect(404); + }); + }); + + describe('constructor option for sending in a custom basePath', () => { + let proxyServerWithFooBasePath: BasePathProxyServer; + let proxyWithFooBasePath: supertest.SuperTest; + + beforeEach(async () => { + // setup and start a proxy server which uses a basePath of "foo" + const proxyConfig = { ...config, port: 10004, basePath: '/foo' }; // <-- "foo" here in basePath + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServerWithFooBasePath = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServerWithFooBasePath.start(options); + proxyWithFooBasePath = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await proxyServerWithFooBasePath.stop(); + }); + + test('it will do a redirect to foo which is our passed in value for the configuration', async () => { + const res = await proxyWithFooBasePath.get('/bar').expect(302); + const location = res.header.location; + expect(location).toEqual('/foo/'); + }); + }); +}); diff --git a/src/core/server/http/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts similarity index 90% rename from src/core/server/http/base_path_proxy_server.ts rename to packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts index a5ed027189393..40841c8327cc2 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts @@ -8,21 +8,21 @@ import Url from 'url'; import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; - import apm from 'elastic-apm-node'; -import { ByteSizeValue } from '@kbn/config-schema'; import { Server, Request } from '@hapi/hapi'; import HapiProxy from '@hapi/h2o2'; import { sampleSize } from 'lodash'; import * as Rx from 'rxjs'; import { take } from 'rxjs/operators'; +import { ByteSizeValue } from '@kbn/config-schema'; +import { createServer, getListenerOptions, getServerOptions } from '@kbn/server-http-tools'; -import { DevConfig } from '../dev'; -import { Logger } from '../logging'; -import { HttpConfig } from './http_config'; -import { createServer, getListenerOptions, getServerOptions } from './http_tools'; +import { DevConfig, HttpConfig } from './config'; +import { Log } from './log'; +const ONE_GIGABYTE = 1024 * 1024 * 1024; const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); +const getRandomBasePath = () => sampleSize(alphabet, 3).join(''); export interface BasePathProxyServerOptions { shouldRedirectFromOldBasePath: (path: string) => boolean; @@ -30,9 +30,22 @@ export interface BasePathProxyServerOptions { } export class BasePathProxyServer { + private readonly httpConfig: HttpConfig; private server?: Server; private httpsAgent?: HttpsAgent; + constructor( + private readonly log: Log, + httpConfig: HttpConfig, + private readonly devConfig: DevConfig + ) { + this.httpConfig = { + ...httpConfig, + maxPayload: new ByteSizeValue(ONE_GIGABYTE), + basePath: httpConfig.basePath ?? `/${getRandomBasePath()}`, + }; + } + public get basePath() { return this.httpConfig.basePath; } @@ -49,21 +62,8 @@ export class BasePathProxyServer { return this.httpConfig.port; } - constructor( - private readonly log: Logger, - private readonly httpConfig: HttpConfig, - private readonly devConfig: DevConfig - ) { - const ONE_GIGABYTE = 1024 * 1024 * 1024; - httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); - - if (!httpConfig.basePath) { - httpConfig.basePath = `/${sampleSize(alphabet, 3).join('')}`; - } - } - - public async start(options: Readonly) { - this.log.debug('starting basepath proxy server'); + public async start(options: BasePathProxyServerOptions) { + this.log.write('starting basepath proxy server'); const serverOptions = getServerOptions(this.httpConfig); const listenerOptions = getListenerOptions(this.httpConfig); @@ -88,7 +88,7 @@ export class BasePathProxyServer { await this.server.start(); - this.log.info( + this.log.write( `basepath proxy server running at ${Url.format({ host: this.server.info.uri, pathname: this.httpConfig.basePath, @@ -101,7 +101,7 @@ export class BasePathProxyServer { return; } - this.log.debug('stopping basepath proxy server'); + this.log.write('stopping basepath proxy server'); await this.server.stop(); this.server = undefined; diff --git a/packages/kbn-cli-dev-mode/src/bootstrap.ts b/packages/kbn-cli-dev-mode/src/bootstrap.ts new file mode 100644 index 0000000000000..86a276c64f1f5 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/bootstrap.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { REPO_ROOT } from '@kbn/utils'; +import { CliArgs, Env, RawConfigAdapter } from '@kbn/config'; +import { CliDevMode } from './cli_dev_mode'; +import { CliLog } from './log'; +import { convertToLogger } from './log_adapter'; +import { loadConfig } from './config'; + +interface BootstrapArgs { + configs: string[]; + cliArgs: CliArgs; + applyConfigOverrides: RawConfigAdapter; +} + +export async function bootstrapDevMode({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs) { + const log = new CliLog(!!cliArgs.quiet, !!cliArgs.silent); + + const env = Env.createDefault(REPO_ROOT, { + configs, + cliArgs, + }); + + const config = await loadConfig({ + env, + logger: convertToLogger(log), + rawConfigAdapter: applyConfigOverrides, + }); + + const cliDevMode = new CliDevMode({ + cliArgs, + config, + log, + }); + + await cliDevMode.start(); +} diff --git a/src/dev/cli_dev_mode/cli_dev_mode.test.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts similarity index 76% rename from src/dev/cli_dev_mode/cli_dev_mode.test.ts rename to packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts index 54c49ce21505f..d5bafe7280bd9 100644 --- a/src/dev/cli_dev_mode/cli_dev_mode.test.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts @@ -7,16 +7,16 @@ */ import Path from 'path'; - +import * as Rx from 'rxjs'; import { REPO_ROOT, createAbsolutePathSerializer, createAnyInstanceSerializer, } from '@kbn/dev-utils'; -import * as Rx from 'rxjs'; import { TestLog } from './log'; -import { CliDevMode } from './cli_dev_mode'; +import { CliDevMode, SomeCliArgs } from './cli_dev_mode'; +import type { CliDevConfig } from './config'; expect.addSnapshotSerializer(createAbsolutePathSerializer()); expect.addSnapshotSerializer(createAnyInstanceSerializer(Rx.Observable, 'Rx.Observable')); @@ -31,6 +31,12 @@ const { Optimizer } = jest.requireMock('./optimizer'); jest.mock('./dev_server'); const { DevServer } = jest.requireMock('./dev_server'); +jest.mock('./base_path_proxy_server'); +const { BasePathProxyServer } = jest.requireMock('./base_path_proxy_server'); + +jest.mock('@kbn/dev-utils/target/ci_stats_reporter'); +const { CiStatsReporter } = jest.requireMock('@kbn/dev-utils/target/ci_stats_reporter'); + jest.mock('./get_server_watch_paths', () => ({ getServerWatchPaths: jest.fn(() => ({ watchPaths: [''], @@ -38,13 +44,6 @@ jest.mock('./get_server_watch_paths', () => ({ })), })); -beforeEach(() => { - process.argv = ['node', './script', 'foo', 'bar', 'baz']; - jest.clearAllMocks(); -}); - -const log = new TestLog(); - const mockBasePathProxy = { targetPort: 9999, basePath: '/foo/bar', @@ -52,26 +51,53 @@ const mockBasePathProxy = { stop: jest.fn(), }; -const defaultOptions = { +let log: TestLog; + +beforeEach(() => { + process.argv = ['node', './script', 'foo', 'bar', 'baz']; + log = new TestLog(); + BasePathProxyServer.mockImplementation(() => mockBasePathProxy); +}); + +afterEach(() => { + jest.clearAllMocks(); + mockBasePathProxy.start.mockReset(); + mockBasePathProxy.stop.mockReset(); +}); + +const createCliArgs = (parts: Partial = {}): SomeCliArgs => ({ + basePath: false, cache: true, disableOptimizer: false, dist: true, oss: true, - pluginPaths: [], - pluginScanDirs: [Path.resolve(REPO_ROOT, 'src/plugins')], - quiet: false, - silent: false, runExamples: false, watch: true, - log, -}; + silent: false, + quiet: false, + ...parts, +}); -afterEach(() => { - log.messages.length = 0; +const createDevConfig = (parts: Partial = {}): CliDevConfig => ({ + plugins: { + pluginSearchPaths: [Path.resolve(REPO_ROOT, 'src/plugins')], + additionalPluginPaths: [], + }, + dev: { + basePathProxyTargetPort: 9000, + }, + http: {} as any, + ...parts, +}); + +const createOptions = ({ cliArgs = {} }: { cliArgs?: Partial } = {}) => ({ + cliArgs: createCliArgs(cliArgs), + config: createDevConfig(), + log, }); it('passes correct args to sub-classes', () => { - new CliDevMode(defaultOptions); + new CliDevMode(createOptions()); expect(DevServer.mock.calls).toMatchInlineSnapshot(` Array [ @@ -102,6 +128,9 @@ it('passes correct args to sub-classes', () => { "enabled": true, "oss": true, "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + ], "quiet": false, "repoRoot": , "runExamples": false, @@ -128,33 +157,38 @@ it('passes correct args to sub-classes', () => { ], ] `); + + expect(BasePathProxyServer).not.toHaveBeenCalled(); + expect(log.messages).toMatchInlineSnapshot(`Array []`); }); it('disables the optimizer', () => { - new CliDevMode({ - ...defaultOptions, - disableOptimizer: true, - }); + new CliDevMode(createOptions({ cliArgs: { disableOptimizer: true } })); expect(Optimizer.mock.calls[0][0]).toHaveProperty('enabled', false); }); it('disables the watcher', () => { - new CliDevMode({ - ...defaultOptions, - watch: false, - }); + new CliDevMode(createOptions({ cliArgs: { watch: false } })); expect(Optimizer.mock.calls[0][0]).toHaveProperty('watch', false); expect(Watcher.mock.calls[0][0]).toHaveProperty('enabled', false); }); -it('overrides the basePath of the server when basePathProxy is defined', () => { - new CliDevMode({ - ...defaultOptions, - basePathProxy: mockBasePathProxy as any, - }); +it('enables the basePath proxy', () => { + new CliDevMode(createOptions({ cliArgs: { basePath: true } })); + + expect(BasePathProxyServer).toHaveBeenCalledTimes(1); + expect(BasePathProxyServer.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + , + Object {}, + Object { + "basePathProxyTargetPort": 9000, + }, + ] + `); expect(DevServer.mock.calls[0][0].argv).toMatchInlineSnapshot(` Array [ @@ -208,6 +242,11 @@ describe('#start()/#stop()', () => { run$: devServerRun$, }; }); + CiStatsReporter.fromEnv.mockImplementation(() => { + return { + isEnabled: jest.fn().mockReturnValue(false), + }; + }); }); afterEach(() => { @@ -221,9 +260,7 @@ describe('#start()/#stop()', () => { }); it('logs a warning if basePathProxy is not passed', () => { - new CliDevMode({ - ...defaultOptions, - }).start(); + new CliDevMode(createOptions()).start(); expect(log.messages).toMatchInlineSnapshot(` Array [ @@ -253,16 +290,9 @@ describe('#start()/#stop()', () => { }); it('calls start on BasePathProxy if enabled', () => { - const basePathProxy: any = { - start: jest.fn(), - }; + new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start(); - new CliDevMode({ - ...defaultOptions, - basePathProxy, - }).start(); - - expect(basePathProxy.start.mock.calls).toMatchInlineSnapshot(` + expect(mockBasePathProxy.start.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -275,7 +305,7 @@ describe('#start()/#stop()', () => { }); it('subscribes to Optimizer#run$, Watcher#run$, and DevServer#run$', () => { - new CliDevMode(defaultOptions).start(); + new CliDevMode(createOptions()).start(); expect(optimizerRun$.observers).toHaveLength(1); expect(watcherRun$.observers).toHaveLength(1); @@ -283,10 +313,7 @@ describe('#start()/#stop()', () => { }); it('logs an error and exits the process if Optimizer#run$ errors', () => { - new CliDevMode({ - ...defaultOptions, - basePathProxy: mockBasePathProxy as any, - }).start(); + new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start(); expect(processExitMock).not.toHaveBeenCalled(); optimizerRun$.error({ stack: 'Error: foo bar' }); @@ -311,10 +338,7 @@ describe('#start()/#stop()', () => { }); it('logs an error and exits the process if Watcher#run$ errors', () => { - new CliDevMode({ - ...defaultOptions, - basePathProxy: mockBasePathProxy as any, - }).start(); + new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start(); expect(processExitMock).not.toHaveBeenCalled(); watcherRun$.error({ stack: 'Error: foo bar' }); @@ -339,10 +363,7 @@ describe('#start()/#stop()', () => { }); it('logs an error and exits the process if DevServer#run$ errors', () => { - new CliDevMode({ - ...defaultOptions, - basePathProxy: mockBasePathProxy as any, - }).start(); + new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start(); expect(processExitMock).not.toHaveBeenCalled(); devServerRun$.error({ stack: 'Error: foo bar' }); @@ -368,10 +389,7 @@ describe('#start()/#stop()', () => { it('throws if start() has already been called', () => { expect(() => { - const devMode = new CliDevMode({ - ...defaultOptions, - basePathProxy: mockBasePathProxy as any, - }); + const devMode = new CliDevMode(createOptions({ cliArgs: { basePath: true } })); devMode.start(); devMode.start(); @@ -379,10 +397,7 @@ describe('#start()/#stop()', () => { }); it('unsubscribes from all observables and stops basePathProxy when stopped', () => { - const devMode = new CliDevMode({ - ...defaultOptions, - basePathProxy: mockBasePathProxy as any, - }); + const devMode = new CliDevMode(createOptions({ cliArgs: { basePath: true } })); devMode.start(); devMode.stop(); diff --git a/src/dev/cli_dev_mode/cli_dev_mode.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts similarity index 76% rename from src/dev/cli_dev_mode/cli_dev_mode.ts rename to packages/kbn-cli-dev-mode/src/cli_dev_mode.ts index 1eed8b14aed4a..94dbcb9654e8a 100644 --- a/src/dev/cli_dev_mode/cli_dev_mode.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts @@ -7,28 +7,43 @@ */ import Path from 'path'; - -import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils'; import * as Rx from 'rxjs'; -import { map, mapTo, filter, take, tap, distinctUntilChanged, switchMap } from 'rxjs/operators'; - -import { CliArgs } from '../../core/server/config'; -import { LegacyConfig } from '../../core/server/legacy'; -import { BasePathProxyServer } from '../../core/server/http'; +import { + map, + mapTo, + filter, + take, + tap, + distinctUntilChanged, + switchMap, + concatMap, +} from 'rxjs/operators'; +import { CliArgs } from '@kbn/config'; +import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils'; import { Log, CliLog } from './log'; import { Optimizer } from './optimizer'; import { DevServer } from './dev_server'; import { Watcher } from './watcher'; +import { BasePathProxyServer } from './base_path_proxy_server'; import { shouldRedirectFromOldBasePath } from './should_redirect_from_old_base_path'; import { getServerWatchPaths } from './get_server_watch_paths'; +import { CliDevConfig } from './config'; // timeout where the server is allowed to exit gracefully const GRACEFUL_TIMEOUT = 5000; export type SomeCliArgs = Pick< CliArgs, - 'quiet' | 'silent' | 'disableOptimizer' | 'watch' | 'oss' | 'runExamples' | 'cache' | 'dist' + | 'quiet' + | 'silent' + | 'disableOptimizer' + | 'watch' + | 'oss' + | 'runExamples' + | 'cache' + | 'dist' + | 'basePath' >; export interface CliDevModeOptions { @@ -67,49 +82,28 @@ const firstAllTrue = (...sources: Array>) => * */ export class CliDevMode { - static fromCoreServices( - cliArgs: SomeCliArgs, - config: LegacyConfig, - basePathProxy?: BasePathProxyServer - ) { - new CliDevMode({ - quiet: !!cliArgs.quiet, - silent: !!cliArgs.silent, - cache: !!cliArgs.cache, - disableOptimizer: !!cliArgs.disableOptimizer, - dist: !!cliArgs.dist, - oss: !!cliArgs.oss, - runExamples: !!cliArgs.runExamples, - pluginPaths: config.get('plugins.paths'), - pluginScanDirs: config.get('plugins.scanDirs'), - watch: !!cliArgs.watch, - basePathProxy, - }).start(); - } private readonly log: Log; private readonly basePathProxy?: BasePathProxyServer; private readonly watcher: Watcher; private readonly devServer: DevServer; private readonly optimizer: Optimizer; private startTime?: number; - private subscription?: Rx.Subscription; - constructor(options: CliDevModeOptions) { - this.basePathProxy = options.basePathProxy; - this.log = options.log || new CliLog(!!options.quiet, !!options.silent); + constructor({ cliArgs, config, log }: { cliArgs: SomeCliArgs; config: CliDevConfig; log?: Log }) { + this.log = log || new CliLog(!!cliArgs.quiet, !!cliArgs.silent); + + if (cliArgs.basePath) { + this.basePathProxy = new BasePathProxyServer(this.log, config.http, config.dev); + } const { watchPaths, ignorePaths } = getServerWatchPaths({ - pluginPaths: options.pluginPaths ?? [], - pluginScanDirs: [ - ...(options.pluginScanDirs ?? []), - Path.resolve(REPO_ROOT, 'src/plugins'), - Path.resolve(REPO_ROOT, 'x-pack/plugins'), - ], + pluginPaths: config.plugins.additionalPluginPaths, + pluginScanDirs: config.plugins.pluginSearchPaths, }); this.watcher = new Watcher({ - enabled: !!options.watch, + enabled: !!cliArgs.watch, log: this.log, cwd: REPO_ROOT, paths: watchPaths, @@ -124,10 +118,10 @@ export class CliDevMode { script: Path.resolve(REPO_ROOT, 'scripts/kibana'), argv: [ ...process.argv.slice(2).filter((v) => v !== '--no-watch'), - ...(options.basePathProxy + ...(this.basePathProxy ? [ - `--server.port=${options.basePathProxy.targetPort}`, - `--server.basePath=${options.basePathProxy.basePath}`, + `--server.port=${this.basePathProxy.targetPort}`, + `--server.basePath=${this.basePathProxy.basePath}`, '--server.rewriteBasePath=true', ] : []), @@ -144,16 +138,17 @@ export class CliDevMode { }); this.optimizer = new Optimizer({ - enabled: !options.disableOptimizer, + enabled: !cliArgs.disableOptimizer, repoRoot: REPO_ROOT, - oss: options.oss, - pluginPaths: options.pluginPaths, - runExamples: options.runExamples, - cache: options.cache, - dist: options.dist, - quiet: options.quiet, - silent: options.silent, - watch: options.watch, + oss: cliArgs.oss, + pluginPaths: config.plugins.additionalPluginPaths, + pluginScanDirs: config.plugins.pluginSearchPaths, + runExamples: cliArgs.runExamples, + cache: cliArgs.cache, + dist: cliArgs.dist, + quiet: !!cliArgs.quiet, + silent: !!cliArgs.silent, + watch: cliArgs.watch, }); } @@ -167,29 +162,10 @@ export class CliDevMode { this.subscription = new Rx.Subscription(); this.startTime = Date.now(); - this.subscription.add( - this.getStarted$() - .pipe( - switchMap(async (success) => { - const reporter = CiStatsReporter.fromEnv(this.log.toolingLog); - await reporter.timings({ - timings: [ - { - group: 'yarn start', - id: 'started', - ms: Date.now() - this.startTime!, - meta: { success }, - }, - ], - }); - }) - ) - .subscribe({ - error: (error) => { - this.log.bad(`[ci-stats/timings] unable to record startup time:`, error.stack); - }, - }) - ); + const reporter = CiStatsReporter.fromEnv(this.log.toolingLog); + if (reporter.isEnabled()) { + this.subscription.add(this.reportTimings(reporter)); + } if (basePathProxy) { const serverReady$ = new Rx.BehaviorSubject(false); @@ -245,6 +221,64 @@ export class CliDevMode { this.subscription.add(this.devServer.run$.subscribe(this.observer('dev server'))); } + private reportTimings(reporter: CiStatsReporter) { + const sub = new Rx.Subscription(); + + sub.add( + this.getStarted$() + .pipe( + concatMap(async (success) => { + await reporter.timings({ + timings: [ + { + group: 'yarn start', + id: 'started', + ms: Date.now() - this.startTime!, + meta: { success }, + }, + ], + }); + }) + ) + .subscribe({ + error: (error) => { + this.log.bad(`[ci-stats/timings] unable to record startup time:`, error.stack); + }, + }) + ); + + sub.add( + this.devServer + .getRestartTime$() + .pipe( + concatMap(async ({ ms }, i) => { + await reporter.timings({ + timings: [ + { + group: 'yarn start', + id: 'dev server restart', + ms, + meta: { + sequence: i + 1, + }, + }, + ], + }); + }) + ) + .subscribe({ + error: (error) => { + this.log.bad( + `[ci-stats/timings] unable to record dev server restart time:`, + error.stack + ); + }, + }) + ); + + return sub; + } + /** * returns an observable that emits once the dev server and optimizer are started, emits * true if they both started successfully, otherwise false diff --git a/packages/kbn-cli-dev-mode/src/config/dev_config.ts b/packages/kbn-cli-dev-mode/src/config/dev_config.ts new file mode 100644 index 0000000000000..ddb54bb8f3f7c --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/config/dev_config.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 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, TypeOf } from '@kbn/config-schema'; + +export const devConfigSchema = schema.object( + { + basePathProxyTarget: schema.number({ + defaultValue: 5603, + }), + }, + { unknowns: 'ignore' } +); + +export type DevConfigType = TypeOf; + +export class DevConfig { + public basePathProxyTargetPort: number; + + constructor(rawConfig: DevConfigType) { + this.basePathProxyTargetPort = rawConfig.basePathProxyTarget; + } +} diff --git a/packages/kbn-cli-dev-mode/src/config/http_config.ts b/packages/kbn-cli-dev-mode/src/config/http_config.ts new file mode 100644 index 0000000000000..34f208c28df68 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/config/http_config.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; +import { ICorsConfig, IHttpConfig, ISslConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; + +export const httpConfigSchema = schema.object( + { + host: schema.string({ + defaultValue: 'localhost', + hostname: true, + }), + basePath: schema.maybe(schema.string()), + port: schema.number({ + defaultValue: 5601, + }), + maxPayload: schema.byteSize({ + defaultValue: '1048576b', + }), + keepaliveTimeout: schema.number({ + defaultValue: 120000, + }), + socketTimeout: schema.number({ + defaultValue: 120000, + }), + cors: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + allowCredentials: schema.boolean({ defaultValue: false }), + allowOrigin: schema.arrayOf(schema.string(), { + defaultValue: ['*'], + }), + }), + ssl: sslSchema, + }, + { unknowns: 'ignore' } +); + +export type HttpConfigType = TypeOf; + +export class HttpConfig implements IHttpConfig { + basePath?: string; + host: string; + port: number; + maxPayload: ByteSizeValue; + keepaliveTimeout: number; + socketTimeout: number; + cors: ICorsConfig; + ssl: ISslConfig; + + constructor(rawConfig: HttpConfigType) { + this.basePath = rawConfig.basePath; + this.host = rawConfig.host; + this.port = rawConfig.port; + this.maxPayload = rawConfig.maxPayload; + this.keepaliveTimeout = rawConfig.keepaliveTimeout; + this.socketTimeout = rawConfig.socketTimeout; + this.cors = rawConfig.cors; + this.ssl = new SslConfig(rawConfig.ssl); + } +} diff --git a/packages/kbn-cli-dev-mode/src/config/index.ts b/packages/kbn-cli-dev-mode/src/config/index.ts new file mode 100644 index 0000000000000..89f6d647ef4f5 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/config/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 type { DevConfig } from './dev_config'; +export type { PluginsConfig } from './plugins_config'; +export type { HttpConfig } from './http_config'; +export type { CliDevConfig } from './types'; +export { loadConfig } from './load_config'; diff --git a/packages/kbn-cli-dev-mode/src/config/load_config.ts b/packages/kbn-cli-dev-mode/src/config/load_config.ts new file mode 100644 index 0000000000000..073cd3dbd4b4c --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/config/load_config.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 { Env, RawConfigService, ConfigService, RawConfigAdapter } from '@kbn/config'; +import { Logger } from '@kbn/logging'; +import { devConfigSchema, DevConfig, DevConfigType } from './dev_config'; +import { httpConfigSchema, HttpConfig, HttpConfigType } from './http_config'; +import { pluginsConfigSchema, PluginsConfig, PluginsConfigType } from './plugins_config'; +import { CliDevConfig } from './types'; + +export const loadConfig = async ({ + env, + logger, + rawConfigAdapter, +}: { + env: Env; + logger: Logger; + rawConfigAdapter: RawConfigAdapter; +}): Promise => { + const rawConfigService = new RawConfigService(env.configs, rawConfigAdapter); + rawConfigService.loadConfig(); + + const configService = new ConfigService(rawConfigService, env, logger); + configService.setSchema('dev', devConfigSchema); + configService.setSchema('plugins', pluginsConfigSchema); + configService.setSchema('server', httpConfigSchema); + + await configService.validate(); + + const devConfig = configService.atPathSync('dev'); + const pluginsConfig = configService.atPathSync('plugins'); + const httpConfig = configService.atPathSync('server'); + + return { + dev: new DevConfig(devConfig), + plugins: new PluginsConfig(pluginsConfig, env), + http: new HttpConfig(httpConfig), + }; +}; diff --git a/packages/kbn-cli-dev-mode/src/config/plugins_config.ts b/packages/kbn-cli-dev-mode/src/config/plugins_config.ts new file mode 100644 index 0000000000000..7c7fa8902edb3 --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/config/plugins_config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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, TypeOf } from '@kbn/config-schema'; +import { Env } from '@kbn/config'; + +export const pluginsConfigSchema = schema.object( + { + paths: schema.arrayOf(schema.string(), { defaultValue: [] }), + }, + { unknowns: 'ignore' } +); + +export type PluginsConfigType = TypeOf; + +/** @internal */ +export class PluginsConfig { + /** + * Defines directories that we should scan for the plugin subdirectories. + */ + public readonly pluginSearchPaths: string[]; + + /** + * Defines directories where an additional plugin exists. + */ + public readonly additionalPluginPaths: string[]; + + constructor(rawConfig: PluginsConfigType, env: Env) { + this.pluginSearchPaths = [...env.pluginSearchPaths]; + this.additionalPluginPaths = rawConfig.paths; + } +} diff --git a/packages/kbn-cli-dev-mode/src/config/types.ts b/packages/kbn-cli-dev-mode/src/config/types.ts new file mode 100644 index 0000000000000..017442e09bd0d --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/config/types.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 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 { DevConfig } from './dev_config'; +import type { HttpConfig } from './http_config'; +import type { PluginsConfig } from './plugins_config'; + +export interface CliDevConfig { + dev: DevConfig; + http: HttpConfig; + plugins: PluginsConfig; +} diff --git a/src/dev/cli_dev_mode/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts similarity index 73% rename from src/dev/cli_dev_mode/dev_server.test.ts rename to packages/kbn-cli-dev-mode/src/dev_server.test.ts index c296c7caca63a..9962a9a285a42 100644 --- a/src/dev/cli_dev_mode/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -15,6 +15,8 @@ import { extendedEnvSerializer } from './test_helpers'; import { DevServer, Options } from './dev_server'; import { TestLog } from './log'; +jest.useFakeTimers('modern'); + class MockProc extends EventEmitter { public readonly signalsSent: string[] = []; @@ -91,6 +93,17 @@ const run = (server: DevServer) => { return subscription; }; +const collect = (stream: Rx.Observable) => { + const events: T[] = []; + const subscription = stream.subscribe({ + next(item) { + events.push(item); + }, + }); + subscriptions.push(subscription); + return events; +}; + afterEach(() => { if (currentProc) { currentProc.removeAllListeners(); @@ -107,6 +120,9 @@ describe('#run$', () => { it('starts the dev server with the right options', () => { run(new DevServer(defaultOptions)).unsubscribe(); + // ensure that FORCE_COLOR is in the env for consistency in snapshot + process.env.FORCE_COLOR = process.env.FORCE_COLOR || 'true'; + expect(execa.node.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -305,7 +321,106 @@ describe('#run$', () => { expect(currentProc.signalsSent).toEqual([]); sigint$.next(); expect(currentProc.signalsSent).toEqual(['SIGINT']); - await new Promise((resolve) => setTimeout(resolve, 1000)); + jest.advanceTimersByTime(100); expect(currentProc.signalsSent).toEqual(['SIGINT', 'SIGKILL']); }); }); + +describe('#getPhase$', () => { + it('emits "starting" when run$ is subscribed then emits "fatal exit" when server exits with code > 0, then starting once watcher fires and "listening" when the server is ready', () => { + const server = new DevServer(defaultOptions); + const events = collect(server.getPhase$()); + + expect(events).toEqual([]); + run(server); + expect(events).toEqual(['starting']); + events.length = 0; + + isProc(currentProc); + currentProc.mockExit(2); + expect(events).toEqual(['fatal exit']); + events.length = 0; + + restart$.next(); + expect(events).toEqual(['starting']); + events.length = 0; + + currentProc.mockListening(); + expect(events).toEqual(['listening']); + }); +}); + +describe('#getRestartTime$()', () => { + it('does not send event if server does not start listening before starting again', () => { + const server = new DevServer(defaultOptions); + const phases = collect(server.getPhase$()); + const events = collect(server.getRestartTime$()); + run(server); + + isProc(currentProc); + restart$.next(); + jest.advanceTimersByTime(1000); + restart$.next(); + jest.advanceTimersByTime(1000); + restart$.next(); + expect(phases).toMatchInlineSnapshot(` + Array [ + "starting", + "starting", + "starting", + "starting", + ] + `); + expect(events).toEqual([]); + }); + + it('reports restart times', () => { + const server = new DevServer(defaultOptions); + const phases = collect(server.getPhase$()); + const events = collect(server.getRestartTime$()); + + run(server); + isProc(currentProc); + + restart$.next(); + currentProc.mockExit(1); + restart$.next(); + restart$.next(); + restart$.next(); + currentProc.mockExit(1); + restart$.next(); + jest.advanceTimersByTime(1234); + currentProc.mockListening(); + restart$.next(); + restart$.next(); + jest.advanceTimersByTime(5678); + currentProc.mockListening(); + + expect(phases).toMatchInlineSnapshot(` + Array [ + "starting", + "starting", + "fatal exit", + "starting", + "starting", + "starting", + "fatal exit", + "starting", + "listening", + "starting", + "starting", + "listening", + ] + `); + expect(events).toMatchInlineSnapshot(` + Array [ + Object { + "ms": 1234, + }, + Object { + "ms": 5678, + }, + ] + `); + }); +}); diff --git a/src/dev/cli_dev_mode/dev_server.ts b/packages/kbn-cli-dev-mode/src/dev_server.ts similarity index 92% rename from src/dev/cli_dev_mode/dev_server.ts rename to packages/kbn-cli-dev-mode/src/dev_server.ts index a4e32a40665e3..3daf298c82324 100644 --- a/src/dev/cli_dev_mode/dev_server.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.ts @@ -16,6 +16,7 @@ import { share, mergeMap, switchMap, + scan, takeUntil, ignoreElements, } from 'rxjs/operators'; @@ -73,6 +74,32 @@ export class DevServer { return this.phase$.asObservable(); } + /** + * returns an observable of objects describing server start time. + */ + getRestartTime$() { + return this.phase$.pipe( + scan((acc: undefined | { phase: string; time: number }, phase) => { + if (phase === 'starting') { + return { phase, time: Date.now() }; + } + + if (phase === 'listening' && acc?.phase === 'starting') { + return { phase, time: Date.now() - acc.time }; + } + + return undefined; + }, undefined), + mergeMap((desc) => { + if (desc?.phase !== 'listening') { + return []; + } + + return [{ ms: desc.time }]; + }) + ); + } + /** * Run the Kibana server * diff --git a/src/dev/cli_dev_mode/get_active_inspect_flag.ts b/packages/kbn-cli-dev-mode/src/get_active_inspect_flag.ts similarity index 100% rename from src/dev/cli_dev_mode/get_active_inspect_flag.ts rename to packages/kbn-cli-dev-mode/src/get_active_inspect_flag.ts diff --git a/src/dev/cli_dev_mode/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts similarity index 100% rename from src/dev/cli_dev_mode/get_server_watch_paths.test.ts rename to packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts diff --git a/src/dev/cli_dev_mode/get_server_watch_paths.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts similarity index 87% rename from src/dev/cli_dev_mode/get_server_watch_paths.ts rename to packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts index 46aa15659a513..53aa53b5aa63a 100644 --- a/src/dev/cli_dev_mode/get_server_watch_paths.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts @@ -47,15 +47,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) { ...pluginScanDirs, ].map((path) => Path.resolve(path)) ) - ); - - for (const watchPath of watchPaths) { - if (!Fs.existsSync(fromRoot(watchPath))) { - throw new Error( - `A watch directory [${watchPath}] does not exist, which will cause chokidar to fail. Either make sure the directory exists or remove it as a watch source in the ClusterManger` - ); - } - } + ).filter((path) => Fs.existsSync(fromRoot(path))); const ignorePaths = [ /[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/, diff --git a/src/dev/cli_dev_mode/index.ts b/packages/kbn-cli-dev-mode/src/index.ts similarity index 86% rename from src/dev/cli_dev_mode/index.ts rename to packages/kbn-cli-dev-mode/src/index.ts index db46957504b11..98b52087f231a 100644 --- a/src/dev/cli_dev_mode/index.ts +++ b/packages/kbn-cli-dev-mode/src/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export * from './cli_dev_mode'; -export * from './log'; +export { bootstrapDevMode } from './bootstrap'; diff --git a/src/dev/cli_dev_mode/log.ts b/packages/kbn-cli-dev-mode/src/log.ts similarity index 100% rename from src/dev/cli_dev_mode/log.ts rename to packages/kbn-cli-dev-mode/src/log.ts diff --git a/packages/kbn-cli-dev-mode/src/log_adapter.ts b/packages/kbn-cli-dev-mode/src/log_adapter.ts new file mode 100644 index 0000000000000..65161fcc56e0e --- /dev/null +++ b/packages/kbn-cli-dev-mode/src/log_adapter.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 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 { Logger } from '@kbn/logging'; +import { Log } from './log'; + +export const convertToLogger = (cliLog: Log): Logger => { + const getErrorMessage = (msgOrError: string | Error): string => { + return typeof msgOrError === 'string' ? msgOrError : msgOrError.message; + }; + + const adapter: Logger = { + trace: (message) => cliLog.write(message), + debug: (message) => cliLog.write(message), + info: (message) => cliLog.write(message), + warn: (msgOrError) => cliLog.warn('warning', getErrorMessage(msgOrError)), + error: (msgOrError) => cliLog.bad('error', getErrorMessage(msgOrError)), + fatal: (msgOrError) => cliLog.bad('fatal', getErrorMessage(msgOrError)), + log: (record) => cliLog.write(record.message), + get: () => adapter, + }; + return adapter; +}; diff --git a/src/dev/cli_dev_mode/optimizer.test.ts b/packages/kbn-cli-dev-mode/src/optimizer.test.ts similarity index 96% rename from src/dev/cli_dev_mode/optimizer.test.ts rename to packages/kbn-cli-dev-mode/src/optimizer.test.ts index 409ad1a455a57..c270a00329897 100644 --- a/src/dev/cli_dev_mode/optimizer.test.ts +++ b/packages/kbn-cli-dev-mode/src/optimizer.test.ts @@ -43,6 +43,7 @@ const defaultOptions: Options = { dist: true, oss: true, pluginPaths: ['/some/dir'], + pluginScanDirs: ['/some-scan-path'], quiet: true, silent: true, repoRoot: '/app', @@ -83,6 +84,7 @@ it('uses options to create valid OptimizerConfig', () => { runExamples: false, oss: false, pluginPaths: [], + pluginScanDirs: [], repoRoot: '/foo/bar', watch: false, }); @@ -99,6 +101,9 @@ it('uses options to create valid OptimizerConfig', () => { "pluginPaths": Array [ "/some/dir", ], + "pluginScanDirs": Array [ + "/some-scan-path", + ], "repoRoot": "/app", "watch": true, }, @@ -111,6 +116,7 @@ it('uses options to create valid OptimizerConfig', () => { "includeCoreBundle": true, "oss": false, "pluginPaths": Array [], + "pluginScanDirs": Array [], "repoRoot": "/foo/bar", "watch": false, }, diff --git a/src/dev/cli_dev_mode/optimizer.ts b/packages/kbn-cli-dev-mode/src/optimizer.ts similarity index 97% rename from src/dev/cli_dev_mode/optimizer.ts rename to packages/kbn-cli-dev-mode/src/optimizer.ts index 771da21e6151b..5e2f16fcf7daa 100644 --- a/src/dev/cli_dev_mode/optimizer.ts +++ b/packages/kbn-cli-dev-mode/src/optimizer.ts @@ -31,6 +31,7 @@ export interface Options { oss: boolean; runExamples: boolean; pluginPaths: string[]; + pluginScanDirs: string[]; writeLogTo?: Writable; } @@ -56,6 +57,7 @@ export class Optimizer { oss: options.oss, examples: options.runExamples, pluginPaths: options.pluginPaths, + pluginScanDirs: options.pluginScanDirs, }); const dim = Chalk.dim('np bld'); diff --git a/src/dev/cli_dev_mode/should_redirect_from_old_base_path.test.ts b/packages/kbn-cli-dev-mode/src/should_redirect_from_old_base_path.test.ts similarity index 100% rename from src/dev/cli_dev_mode/should_redirect_from_old_base_path.test.ts rename to packages/kbn-cli-dev-mode/src/should_redirect_from_old_base_path.test.ts diff --git a/src/dev/cli_dev_mode/should_redirect_from_old_base_path.ts b/packages/kbn-cli-dev-mode/src/should_redirect_from_old_base_path.ts similarity index 100% rename from src/dev/cli_dev_mode/should_redirect_from_old_base_path.ts rename to packages/kbn-cli-dev-mode/src/should_redirect_from_old_base_path.ts diff --git a/src/dev/cli_dev_mode/test_helpers.ts b/packages/kbn-cli-dev-mode/src/test_helpers.ts similarity index 100% rename from src/dev/cli_dev_mode/test_helpers.ts rename to packages/kbn-cli-dev-mode/src/test_helpers.ts diff --git a/src/dev/cli_dev_mode/using_server_process.ts b/packages/kbn-cli-dev-mode/src/using_server_process.ts similarity index 100% rename from src/dev/cli_dev_mode/using_server_process.ts rename to packages/kbn-cli-dev-mode/src/using_server_process.ts diff --git a/src/dev/cli_dev_mode/watcher.test.ts b/packages/kbn-cli-dev-mode/src/watcher.test.ts similarity index 100% rename from src/dev/cli_dev_mode/watcher.test.ts rename to packages/kbn-cli-dev-mode/src/watcher.test.ts diff --git a/src/dev/cli_dev_mode/watcher.ts b/packages/kbn-cli-dev-mode/src/watcher.ts similarity index 100% rename from src/dev/cli_dev_mode/watcher.ts rename to packages/kbn-cli-dev-mode/src/watcher.ts diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json new file mode 100644 index 0000000000000..b2bdaf8ceea36 --- /dev/null +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./target", + "declarationMap": true, + "types": ["jest", "node"] + }, + "include": ["./src/**/*.ts"], + "exclude": ["target"] +} diff --git a/packages/kbn-config/src/__mocks__/env.ts b/packages/kbn-config/src/__mocks__/env.ts index e3b3106933f1e..6f05f8f1f5a45 100644 --- a/packages/kbn-config/src/__mocks__/env.ts +++ b/packages/kbn-config/src/__mocks__/env.ts @@ -30,6 +30,5 @@ export function getEnvOptions(options: DeepPartial = {}): EnvOptions runExamples: false, ...(options.cliArgs || {}), }, - isDevCliParent: options.isDevCliParent !== undefined ? options.isDevCliParent : false, }; } diff --git a/packages/kbn-config/src/__snapshots__/env.test.ts.snap b/packages/kbn-config/src/__snapshots__/env.test.ts.snap index fae14529a4af3..570ed948774cc 100644 --- a/packages/kbn-config/src/__snapshots__/env.test.ts.snap +++ b/packages/kbn-config/src/__snapshots__/env.test.ts.snap @@ -21,7 +21,6 @@ Env { "/some/other/path/some-kibana.yml", ], "homeDir": "/test/kibanaRoot", - "isDevCliParent": false, "logDir": "/test/kibanaRoot/log", "mode": Object { "dev": true, @@ -65,7 +64,6 @@ Env { "/some/other/path/some-kibana.yml", ], "homeDir": "/test/kibanaRoot", - "isDevCliParent": false, "logDir": "/test/kibanaRoot/log", "mode": Object { "dev": false, @@ -108,7 +106,6 @@ Env { "/test/cwd/config/kibana.yml", ], "homeDir": "/test/kibanaRoot", - "isDevCliParent": true, "logDir": "/test/kibanaRoot/log", "mode": Object { "dev": true, @@ -151,7 +148,6 @@ Env { "/some/other/path/some-kibana.yml", ], "homeDir": "/test/kibanaRoot", - "isDevCliParent": false, "logDir": "/test/kibanaRoot/log", "mode": Object { "dev": false, @@ -194,7 +190,6 @@ Env { "/some/other/path/some-kibana.yml", ], "homeDir": "/test/kibanaRoot", - "isDevCliParent": false, "logDir": "/test/kibanaRoot/log", "mode": Object { "dev": false, @@ -237,7 +232,6 @@ Env { "/some/other/path/some-kibana.yml", ], "homeDir": "/some/home/dir", - "isDevCliParent": false, "logDir": "/some/home/dir/log", "mode": Object { "dev": false, diff --git a/packages/kbn-config/src/config_service.mock.ts b/packages/kbn-config/src/config_service.mock.ts index 638627caf1e50..83fbf20b5c0b3 100644 --- a/packages/kbn-config/src/config_service.mock.ts +++ b/packages/kbn-config/src/config_service.mock.ts @@ -25,13 +25,16 @@ const createConfigServiceMock = ({ setSchema: jest.fn(), addDeprecationProvider: jest.fn(), validate: jest.fn(), + getHandledDeprecatedConfigs: jest.fn(), }; + mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); mocked.atPathSync.mockReturnValue(atPath); mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); mocked.getUsedPaths.mockResolvedValue([]); mocked.getUnusedPaths.mockResolvedValue([]); mocked.isEnabledAtPath.mockResolvedValue(true); + mocked.getHandledDeprecatedConfigs.mockReturnValue([]); return mocked; }; diff --git a/packages/kbn-config/src/config_service.test.mocks.ts b/packages/kbn-config/src/config_service.test.mocks.ts index 99539726c3e43..d8da2852b9251 100644 --- a/packages/kbn-config/src/config_service.test.mocks.ts +++ b/packages/kbn-config/src/config_service.test.mocks.ts @@ -7,9 +7,15 @@ */ export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); +import type { applyDeprecations } from './deprecation/apply_deprecations'; + jest.mock('../../../package.json', () => mockPackage); -export const mockApplyDeprecations = jest.fn((config, deprecations, log) => config); +export const mockApplyDeprecations = jest.fn< + Record, + Parameters +>((config, deprecations, createAddDeprecation) => config); + jest.mock('./deprecation/apply_deprecations', () => ({ applyDeprecations: mockApplyDeprecations, })); diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index e38fff866df89..64404341bc64d 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -72,10 +72,10 @@ test('throws if config at path does not match schema', async () => { ); await expect(valuesReceived).toMatchInlineSnapshot(` - Array [ - [Error: [config validation of [key]]: expected value of type [string] but got [number]], - ] - `); + Array [ + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] + `); }); test('re-validate config when updated', async () => { @@ -97,11 +97,11 @@ test('re-validate config when updated', async () => { rawConfig$.next({ key: 123 }); - await expect(valuesReceived).toMatchInlineSnapshot(` - Array [ - "value", - [Error: [config validation of [key]]: expected value of type [string] but got [number]], - ] + expect(valuesReceived).toMatchInlineSnapshot(` + Array [ + "value", + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] `); }); @@ -416,10 +416,10 @@ test('throws during validation is any schema is invalid', async () => { test('logs deprecation warning during validation', async () => { const rawConfig = getRawConfigProvider({}); const configService = new ConfigService(rawConfig, defaultEnv, logger); - - mockApplyDeprecations.mockImplementationOnce((config, deprecations, log) => { - log('some deprecation message'); - log('another deprecation message'); + mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { + const addDeprecation = createAddDeprecation!(''); + addDeprecation({ message: 'some deprecation message' }); + addDeprecation({ message: 'another deprecation message' }); return config; }); @@ -437,6 +437,37 @@ test('logs deprecation warning during validation', async () => { `); }); +test('does not log warnings for silent deprecations during validation', async () => { + const rawConfig = getRawConfigProvider({}); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + + mockApplyDeprecations + .mockImplementationOnce((config, deprecations, createAddDeprecation) => { + const addDeprecation = createAddDeprecation!(''); + addDeprecation({ message: 'some deprecation message', silent: true }); + addDeprecation({ message: 'another deprecation message' }); + return config; + }) + .mockImplementationOnce((config, deprecations, createAddDeprecation) => { + const addDeprecation = createAddDeprecation!(''); + addDeprecation({ message: 'I am silent', silent: true }); + return config; + }); + + loggerMock.clear(logger); + await configService.validate(); + expect(loggerMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "another deprecation message", + ], + ] + `); + loggerMock.clear(logger); + await configService.validate(); + expect(loggerMock.collect(logger).warn).toMatchInlineSnapshot(`Array []`); +}); + describe('atPathSync', () => { test('returns the value at path', async () => { const rawConfig = getRawConfigProvider({ key: 'foo' }); @@ -477,3 +508,36 @@ describe('atPathSync', () => { expect(configService.atPathSync('key')).toEqual('new-value'); }); }); + +describe('getHandledDeprecatedConfigs', () => { + it('returns all handled deprecated configs', async () => { + const rawConfig = getRawConfigProvider({ base: { unused: 'unusedConfig' } }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + + configService.addDeprecationProvider('base', ({ unused }) => [unused('unused')]); + + mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { + deprecations.forEach((deprecation) => { + const addDeprecation = createAddDeprecation!(deprecation.path); + addDeprecation({ message: `some deprecation message`, documentationUrl: 'some-url' }); + }); + return config; + }); + + await configService.validate(); + + expect(configService.getHandledDeprecatedConfigs()).toMatchInlineSnapshot(` + Array [ + Array [ + "base", + Array [ + Object { + "documentationUrl": "some-url", + "message": "some deprecation message", + }, + ], + ], + ] + `); + }); +}); diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index d71327350d212..91927b4c7b5c9 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -21,6 +21,7 @@ import { ConfigDeprecationWithContext, ConfigDeprecationProvider, configDeprecationFactory, + DeprecatedConfigDetails, } from './deprecation'; import { LegacyObjectToConfigAdapter } from './legacy'; @@ -43,6 +44,7 @@ export class ConfigService { private readonly handledPaths: Set = new Set(); private readonly schemas = new Map>(); private readonly deprecations = new BehaviorSubject([]); + private readonly handledDeprecatedConfigs = new Map(); constructor( private readonly rawConfigProvider: RawConfigurationProvider, @@ -91,6 +93,13 @@ export class ConfigService { ]); } + /** + * returns all handled deprecated configs + */ + public getHandledDeprecatedConfigs() { + return [...this.handledDeprecatedConfigs.entries()]; + } + /** * Validate the whole configuration and log the deprecation warnings. * @@ -186,8 +195,16 @@ export class ConfigService { const rawConfig = await this.rawConfigProvider.getConfig$().pipe(take(1)).toPromise(); const deprecations = await this.deprecations.pipe(take(1)).toPromise(); const deprecationMessages: string[] = []; - const logger = (msg: string) => deprecationMessages.push(msg); - applyDeprecations(rawConfig, deprecations, logger); + const createAddDeprecation = (domainId: string) => (context: DeprecatedConfigDetails) => { + if (!context.silent) { + deprecationMessages.push(context.message); + } + const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; + handledDeprecatedConfig.push(context); + this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); + }; + + applyDeprecations(rawConfig, deprecations, createAddDeprecation); deprecationMessages.forEach((msg) => { this.deprecationLog.warn(msg); }); diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts index 9e058faf68052..f2c0a43916343 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts @@ -32,8 +32,31 @@ describe('applyDeprecations', () => { expect(handlerC).toHaveBeenCalledTimes(1); }); + it('passes path to addDeprecation factory', () => { + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + const alteredConfig = { foo: 'bar' }; + + const handlerA = jest.fn().mockReturnValue(alteredConfig); + const handlerB = jest.fn().mockImplementation((conf) => conf); + + applyDeprecations( + initialConfig, + [wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')], + createAddDeprecation + ); + + expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation); + expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation); + expect(createAddDeprecation).toBeCalledTimes(2); + expect(createAddDeprecation).toHaveBeenNthCalledWith(1, 'pathA'); + expect(createAddDeprecation).toHaveBeenNthCalledWith(2, 'pathB'); + }); + it('calls handlers with correct arguments', () => { - const logger = () => undefined; + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; const alteredConfig = { foo: 'bar' }; @@ -43,11 +66,11 @@ describe('applyDeprecations', () => { applyDeprecations( initialConfig, [wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')], - logger + createAddDeprecation ); - expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', logger); - expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', logger); + expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation); + expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation); }); it('returns the migrated config', () => { diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.ts b/packages/kbn-config/src/deprecation/apply_deprecations.ts index 0813440adb57c..6aced541dc30d 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.ts @@ -7,23 +7,24 @@ */ import { cloneDeep } from 'lodash'; -import { ConfigDeprecationWithContext, ConfigDeprecationLogger } from './types'; - -const noopLogger = (msg: string) => undefined; +import { ConfigDeprecationWithContext, AddConfigDeprecation } from './types'; +const noopAddDeprecationFactory: () => AddConfigDeprecation = () => () => undefined; /** - * Applies deprecations on given configuration and logs any deprecation warning using provided logger. + * Applies deprecations on given configuration and passes addDeprecation hook. + * This hook is used for logging any deprecation warning using provided logger. + * This hook is used for exposing deprecated configs that must be handled by the user before upgrading to next major. * * @internal */ export const applyDeprecations = ( config: Record, deprecations: ConfigDeprecationWithContext[], - logger: ConfigDeprecationLogger = noopLogger + createAddDeprecation: (pluginId: string) => AddConfigDeprecation = noopAddDeprecationFactory ) => { let processed = cloneDeep(config); deprecations.forEach(({ deprecation, path }) => { - processed = deprecation(processed, path, logger); + processed = deprecation(processed, path, createAddDeprecation(path)); }); return processed; }; diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index ba8a0cbf7ca57..11a49ed79d170 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -6,17 +6,16 @@ * Side Public License, v 1. */ -import { ConfigDeprecationLogger } from './types'; +import { DeprecatedConfigDetails } from './types'; import { configDeprecationFactory } from './deprecation_factory'; describe('DeprecationFactory', () => { const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; - let deprecationMessages: string[]; - const logger: ConfigDeprecationLogger = (msg) => deprecationMessages.push(msg); + const addDeprecation = jest.fn(); beforeEach(() => { - deprecationMessages = []; + addDeprecation.mockClear(); }); describe('rename', () => { @@ -30,7 +29,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { renamed: 'toberenamed', @@ -40,9 +39,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + }, + ], ] `); }); @@ -56,7 +64,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', logger); + const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { new: 'new', @@ -66,7 +74,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toHaveBeenCalledTimes(0); }); it('handles nested keys', () => { const rawConfig = { @@ -83,7 +91,7 @@ describe('DeprecationFactory', () => { const processed = rename('oldsection.deprecated', 'newsection.renamed')( rawConfig, 'myplugin', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { @@ -97,9 +105,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"myplugin.oldsection.deprecated\\" with \\"myplugin.newsection.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + }, + ], ] `); }); @@ -110,15 +127,25 @@ describe('DeprecationFactory', () => { renamed: 'renamed', }, }; - const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { renamed: 'renamed', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure \\"myplugin.renamed\\" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).", + "Remove \\"myplugin.deprecated\\" from the config.", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + }, + ], ] `); }); @@ -138,7 +165,7 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { @@ -149,9 +176,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + }, + ], ] `); }); @@ -169,7 +205,7 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ oldplugin: { @@ -180,9 +216,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"oldplugin.deprecated\\" with \\"newplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + }, + ], ] `); }); @@ -200,7 +245,7 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('myplugin.deprecated', 'myplugin.new')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { @@ -211,7 +256,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toBeCalledTimes(0); }); it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => { @@ -224,16 +269,27 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { renamed: 'renamed', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure \\"myplugin.renamed\\" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).", + "Remove \\"myplugin.deprecated\\" from the config.", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + }, + ], ] `); }); @@ -250,7 +306,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + const processed = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -259,9 +315,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "myplugin.deprecated is deprecated and is no longer used", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + ], + }, + "message": "myplugin.deprecated is deprecated and is no longer used", + }, + ], ] `); }); @@ -278,7 +343,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('section.deprecated')(rawConfig, 'myplugin', logger); + const processed = unused('section.deprecated')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -288,9 +353,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "myplugin.section.deprecated is deprecated and is no longer used", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + ], + }, + "message": "myplugin.section.deprecated is deprecated and is no longer used", + }, + ], ] `); }); @@ -304,7 +379,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + const processed = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -313,7 +388,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toBeCalledTimes(0); }); }); @@ -328,7 +403,11 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + const processed = unusedFromRoot('myplugin.deprecated')( + rawConfig, + 'does-not-matter', + addDeprecation + ); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -337,9 +416,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "myplugin.deprecated is deprecated and is no longer used", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + ], + }, + "message": "myplugin.deprecated is deprecated and is no longer used", + }, + ], ] `); }); @@ -353,7 +442,11 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + const processed = unusedFromRoot('myplugin.deprecated')( + rawConfig, + 'does-not-matter', + addDeprecation + ); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -362,7 +455,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toBeCalledTimes(0); }); }); }); diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 73196dc897a51..140846d86ae0b 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -9,15 +9,20 @@ import { get } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; import { unset } from '@kbn/std'; -import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types'; +import { + ConfigDeprecation, + AddConfigDeprecation, + ConfigDeprecationFactory, + DeprecatedConfigDetails, +} from './types'; const _rename = ( config: Record, rootPath: string, - log: ConfigDeprecationLogger, + addDeprecation: AddConfigDeprecation, oldKey: string, newKey: string, - silent?: boolean + details?: Partial ) => { const fullOldPath = getPath(rootPath, oldKey); const oldValue = get(config, fullOldPath); @@ -32,48 +37,80 @@ const _rename = ( if (newValue === undefined) { set(config, fullNewPath, oldValue); - if (!silent) { - log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); - } + addDeprecation({ + message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`, + correctiveActions: { + manualSteps: [ + `Replace "${fullOldPath}" with "${fullNewPath}" in the Kibana config file, CLI flag, or environment variable (in Docker only).`, + ], + }, + ...details, + }); } else { - if (!silent) { - log( - `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` - ); - } + addDeprecation({ + message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"`, + correctiveActions: { + manualSteps: [ + `Make sure "${fullNewPath}" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).`, + `Remove "${fullOldPath}" from the config.`, + ], + }, + ...details, + }); } + return config; }; const _unused = ( config: Record, rootPath: string, - log: ConfigDeprecationLogger, - unusedKey: string + addDeprecation: AddConfigDeprecation, + unusedKey: string, + details?: Partial ) => { const fullPath = getPath(rootPath, unusedKey); if (get(config, fullPath) === undefined) { return config; } unset(config, fullPath); - log(`${fullPath} is deprecated and is no longer used`); + addDeprecation({ + message: `${fullPath} is deprecated and is no longer used`, + correctiveActions: { + manualSteps: [ + `Remove "${fullPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only)`, + ], + }, + ...details, + }); return config; }; -const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => - _rename(config, rootPath, log, oldKey, newKey); +const rename = ( + oldKey: string, + newKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _rename(config, rootPath, addDeprecation, oldKey, newKey, details); -const renameFromRoot = (oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation => ( - config, - rootPath, - log -) => _rename(config, '', log, oldKey, newKey, silent); +const renameFromRoot = ( + oldKey: string, + newKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _rename(config, '', addDeprecation, oldKey, newKey, details); -const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => - _unused(config, rootPath, log, unusedKey); +const unused = ( + unusedKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _unused(config, rootPath, addDeprecation, unusedKey, details); -const unusedFromRoot = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => - _unused(config, '', log, unusedKey); +const unusedFromRoot = ( + unusedKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _unused(config, '', addDeprecation, unusedKey, details); const getPath = (rootPath: string, subPath: string) => rootPath !== '' ? `${rootPath}.${subPath}` : subPath; diff --git a/packages/kbn-config/src/deprecation/index.ts b/packages/kbn-config/src/deprecation/index.ts index 6fe1a53efecbc..3286acca9e584 100644 --- a/packages/kbn-config/src/deprecation/index.ts +++ b/packages/kbn-config/src/deprecation/index.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ -export { +export type { ConfigDeprecation, ConfigDeprecationWithContext, - ConfigDeprecationLogger, ConfigDeprecationFactory, + AddConfigDeprecation, ConfigDeprecationProvider, + DeprecatedConfigDetails, } from './types'; export { configDeprecationFactory } from './deprecation_factory'; export { applyDeprecations } from './apply_deprecations'; diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 6e1816867abcf..3b1d004d7ec76 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -7,11 +7,33 @@ */ /** - * Logger interface used when invoking a {@link ConfigDeprecation} + * Config deprecation hook used when invoking a {@link ConfigDeprecation} * * @public */ -export type ConfigDeprecationLogger = (message: string) => void; +export type AddConfigDeprecation = (details: DeprecatedConfigDetails) => void; + +/** + * Deprecated Config Details + * + * @public + */ +export interface DeprecatedConfigDetails { + /* The message to be displayed for the deprecation. */ + message: string; + /* (optional) set false to prevent the config service from logging the deprecation message. */ + silent?: boolean; + /* (optional) link to the documentation for more details on the deprecation. */ + documentationUrl?: string; + /* (optional) corrective action needed to fix this deprecation. */ + correctiveActions?: { + /** + * Specify a list of manual steps our users need to follow + * to fix the deprecation before upgrade. + */ + manualSteps: string[]; + }; +} /** * Configuration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration. @@ -25,7 +47,7 @@ export type ConfigDeprecationLogger = (message: string) => void; export type ConfigDeprecation = ( config: Record, fromPath: string, - logger: ConfigDeprecationLogger + addDeprecation: AddConfigDeprecation ) => Record; /** @@ -62,6 +84,7 @@ export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => C * * @public */ + export interface ConfigDeprecationFactory { /** * Rename a configuration property from inside a plugin's configuration path. @@ -75,7 +98,11 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - rename(oldKey: string, newKey: string): ConfigDeprecation; + rename( + oldKey: string, + newKey: string, + details?: Partial + ): ConfigDeprecation; /** * Rename a configuration property from the root configuration. * Will log a deprecation warning if the oldKey was found and deprecation applied. @@ -91,7 +118,11 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; + renameFromRoot( + oldKey: string, + newKey: string, + details?: Partial + ): ConfigDeprecation; /** * Remove a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the unused key was found and deprecation applied. @@ -104,7 +135,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - unused(unusedKey: string): ConfigDeprecation; + unused(unusedKey: string, details?: Partial): ConfigDeprecation; /** * Remove a configuration property from the root configuration. * Will log a deprecation warning if the unused key was found and deprecation applied. @@ -120,7 +151,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - unusedFromRoot(unusedKey: string): ConfigDeprecation; + unusedFromRoot(unusedKey: string, details?: Partial): ConfigDeprecation; } /** @internal */ diff --git a/packages/kbn-config/src/env.test.ts b/packages/kbn-config/src/env.test.ts index 09d44f31cf8d5..b9e97514c2dff 100644 --- a/packages/kbn-config/src/env.test.ts +++ b/packages/kbn-config/src/env.test.ts @@ -36,7 +36,6 @@ test('correctly creates default environment in dev mode.', () => { REPO_ROOT, getEnvOptions({ configs: ['/test/cwd/config/kibana.yml'], - isDevCliParent: true, }) ); diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index b6ff5e3b5aab2..c4845ab429c57 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -15,7 +15,6 @@ import { PackageInfo, EnvironmentMode } from './types'; export interface EnvOptions { configs: string[]; cliArgs: CliArgs; - isDevCliParent: boolean; } /** @internal */ @@ -89,12 +88,6 @@ export class Env { */ public readonly configs: readonly string[]; - /** - * Indicates that this Kibana instance is running in the parent process of the dev cli. - * @internal - */ - public readonly isDevCliParent: boolean; - /** * @internal */ @@ -111,7 +104,6 @@ export class Env { this.cliArgs = Object.freeze(options.cliArgs); this.configs = Object.freeze(options.configs); - this.isDevCliParent = options.isDevCliParent; const isDevMode = this.cliArgs.dev || this.cliArgs.envName === 'development'; this.mode = Object.freeze({ diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index 24f271c979f32..a9ea8265a3768 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -6,17 +6,22 @@ * Side Public License, v 1. */ -export { - applyDeprecations, - ConfigDeprecation, +export type { ConfigDeprecationFactory, - configDeprecationFactory, - ConfigDeprecationLogger, + AddConfigDeprecation, ConfigDeprecationProvider, ConfigDeprecationWithContext, + ConfigDeprecation, } from './deprecation'; -export { RawConfigurationProvider, RawConfigService, getConfigFromFiles } from './raw'; +export { applyDeprecations, configDeprecationFactory } from './deprecation'; + +export { + RawConfigurationProvider, + RawConfigService, + RawConfigAdapter, + getConfigFromFiles, +} from './raw'; export { ConfigService, IConfigService } from './config_service'; export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; diff --git a/packages/kbn-config/src/raw/index.ts b/packages/kbn-config/src/raw/index.ts index 8f65e7877ba56..01ad83728aa08 100644 --- a/packages/kbn-config/src/raw/index.ts +++ b/packages/kbn-config/src/raw/index.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export { RawConfigService, RawConfigurationProvider } from './raw_config_service'; +export { RawConfigService, RawConfigurationProvider, RawConfigAdapter } from './raw_config_service'; export { getConfigFromFiles } from './read_config'; diff --git a/packages/kbn-config/src/raw/raw_config_service.ts b/packages/kbn-config/src/raw/raw_config_service.ts index af901f2b3d28e..cce1132bebdb0 100644 --- a/packages/kbn-config/src/raw/raw_config_service.ts +++ b/packages/kbn-config/src/raw/raw_config_service.ts @@ -13,7 +13,7 @@ import typeDetect from 'type-detect'; import { getConfigFromFiles } from './read_config'; -type RawConfigAdapter = (rawConfig: Record) => Record; +export type RawConfigAdapter = (rawConfig: Record) => Record; export type RawConfigurationProvider = Pick; diff --git a/packages/kbn-crypto/README.md b/packages/kbn-crypto/README.md new file mode 100644 index 0000000000000..4404c22eba37c --- /dev/null +++ b/packages/kbn-crypto/README.md @@ -0,0 +1,3 @@ +# @kbn/crypto + +Crypto tools and utilities for Kibana \ No newline at end of file diff --git a/packages/kbn-crypto/jest.config.js b/packages/kbn-crypto/jest.config.js new file mode 100644 index 0000000000000..811b87e5ed0f6 --- /dev/null +++ b/packages/kbn-crypto/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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: ['/packages/kbn-crypto'], +}; diff --git a/packages/kbn-crypto/package.json b/packages/kbn-crypto/package.json new file mode 100644 index 0000000000000..6c7b3f3b0c719 --- /dev/null +++ b/packages/kbn-crypto/package.json @@ -0,0 +1,16 @@ +{ + "name": "@kbn/crypto", + "version": "1.0.0", + "private": true, + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": {}, + "devDependencies": { + "@kbn/dev-utils": "link:../kbn-dev-utils" + } +} \ No newline at end of file diff --git a/src/core/server/utils/crypto/__fixtures__/README.md b/packages/kbn-crypto/src/__fixtures__/README.md similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/README.md rename to packages/kbn-crypto/src/__fixtures__/README.md diff --git a/src/core/server/utils/crypto/__fixtures__/index.ts b/packages/kbn-crypto/src/__fixtures__/index.ts similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/index.ts rename to packages/kbn-crypto/src/__fixtures__/index.ts diff --git a/src/core/server/utils/crypto/__fixtures__/no_ca.p12 b/packages/kbn-crypto/src/__fixtures__/no_ca.p12 similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/no_ca.p12 rename to packages/kbn-crypto/src/__fixtures__/no_ca.p12 diff --git a/src/core/server/utils/crypto/__fixtures__/no_cert.p12 b/packages/kbn-crypto/src/__fixtures__/no_cert.p12 similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/no_cert.p12 rename to packages/kbn-crypto/src/__fixtures__/no_cert.p12 diff --git a/src/core/server/utils/crypto/__fixtures__/no_key.p12 b/packages/kbn-crypto/src/__fixtures__/no_key.p12 similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/no_key.p12 rename to packages/kbn-crypto/src/__fixtures__/no_key.p12 diff --git a/src/core/server/utils/crypto/__fixtures__/two_cas.p12 b/packages/kbn-crypto/src/__fixtures__/two_cas.p12 similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/two_cas.p12 rename to packages/kbn-crypto/src/__fixtures__/two_cas.p12 diff --git a/src/core/server/utils/crypto/__fixtures__/two_keys.p12 b/packages/kbn-crypto/src/__fixtures__/two_keys.p12 similarity index 100% rename from src/core/server/utils/crypto/__fixtures__/two_keys.p12 rename to packages/kbn-crypto/src/__fixtures__/two_keys.p12 diff --git a/src/core/server/utils/crypto/index.ts b/packages/kbn-crypto/src/index.ts similarity index 100% rename from src/core/server/utils/crypto/index.ts rename to packages/kbn-crypto/src/index.ts diff --git a/src/core/server/utils/crypto/pkcs12.test.ts b/packages/kbn-crypto/src/pkcs12.test.ts similarity index 99% rename from src/core/server/utils/crypto/pkcs12.test.ts rename to packages/kbn-crypto/src/pkcs12.test.ts index 8c6e5bae3b9c1..ba8eb6554f7b8 100644 --- a/src/core/server/utils/crypto/pkcs12.test.ts +++ b/packages/kbn-crypto/src/pkcs12.test.ts @@ -18,7 +18,7 @@ import { import { NO_CA_PATH, NO_CERT_PATH, NO_KEY_PATH, TWO_CAS_PATH, TWO_KEYS_PATH } from './__fixtures__'; import { readFileSync } from 'fs'; -import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './index'; +import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './pkcs12'; const reformatPem = (pem: string) => { // ensure consistency in line endings when comparing two PEM files diff --git a/src/core/server/utils/crypto/pkcs12.ts b/packages/kbn-crypto/src/pkcs12.ts similarity index 100% rename from src/core/server/utils/crypto/pkcs12.ts rename to packages/kbn-crypto/src/pkcs12.ts diff --git a/src/core/server/utils/crypto/sha256.test.ts b/packages/kbn-crypto/src/sha256.test.ts similarity index 100% rename from src/core/server/utils/crypto/sha256.test.ts rename to packages/kbn-crypto/src/sha256.test.ts diff --git a/src/core/server/utils/crypto/sha256.ts b/packages/kbn-crypto/src/sha256.ts similarity index 100% rename from src/core/server/utils/crypto/sha256.ts rename to packages/kbn-crypto/src/sha256.ts diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json new file mode 100644 index 0000000000000..e9dd6313e6f79 --- /dev/null +++ b/packages/kbn-crypto/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json index 99b108eb2e6b3..e9b87aa0f972f 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json @@ -2,6 +2,45 @@ "id": "pluginA", "client": { "classes": [ + { + "id": "def-public.CrazyClass", + "type": "Class", + "tags": [], + "label": "CrazyClass", + "description": [], + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.CrazyClass", + "text": "CrazyClass" + }, + "

extends ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleClass", + "text": "ExampleClass" + }, + "<", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.WithGen", + "text": "WithGen" + }, + "

>" + ], + "children": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 65 + }, + "initialIsOpen": false + }, { "id": "def-public.ExampleClass", "type": "Class", @@ -35,8 +74,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 44, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L44" + "lineNumber": 44 }, "signature": [ "React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined" @@ -61,8 +99,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 46, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46" + "lineNumber": 46 } } ], @@ -70,8 +107,7 @@ "returnComment": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 46, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46" + "lineNumber": 46 } }, { @@ -94,8 +130,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 54, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L54" + "lineNumber": 54 } } ], @@ -123,8 +158,7 @@ "label": "arrowFn", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 54, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L54" + "lineNumber": 54 }, "tags": [], "returnComment": [] @@ -166,8 +200,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 60, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L60" + "lineNumber": 60 } } ], @@ -175,200 +208,18 @@ "returnComment": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 60, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L60" + "lineNumber": 60 } } ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 38, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L38" - }, - "initialIsOpen": false - }, - { - "id": "def-public.CrazyClass", - "type": "Class", - "tags": [], - "label": "CrazyClass", - "description": [], - "signature": [ - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.CrazyClass", - "text": "CrazyClass" - }, - "

extends ", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.ExampleClass", - "text": "ExampleClass" - }, - "<", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.WithGen", - "text": "WithGen" - }, - "

>" - ], - "children": [], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 65, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L65" + "lineNumber": 38 }, "initialIsOpen": false } ], "functions": [ - { - "id": "def-public.notAnArrowFn", - "type": "Function", - "label": "notAnArrowFn", - "signature": [ - "(a: string, b: number | undefined, c: ", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.TypeWithGeneric", - "text": "TypeWithGeneric" - }, - ", d: ", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.ImAType", - "text": "ImAType" - }, - ", e: string | undefined) => ", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.TypeWithGeneric", - "text": "TypeWithGeneric" - }, - "" - ], - "description": [ - "\nThis is a non arrow function.\n" - ], - "children": [ - { - "type": "string", - "label": "a", - "isRequired": true, - "signature": [ - "string" - ], - "description": [ - "The letter A" - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 22, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L22" - } - }, - { - "type": "number", - "label": "b", - "isRequired": false, - "signature": [ - "number | undefined" - ], - "description": [ - "Feed me to the function" - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 23, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L23" - } - }, - { - "type": "Array", - "label": "c", - "isRequired": true, - "signature": [ - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.TypeWithGeneric", - "text": "TypeWithGeneric" - }, - "" - ], - "description": [ - "So many params" - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 24, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L24" - } - }, - { - "type": "CompoundType", - "label": "d", - "isRequired": true, - "signature": [ - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.ImAType", - "text": "ImAType" - } - ], - "description": [ - "a great param" - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 25, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L25" - } - }, - { - "type": "string", - "label": "e", - "isRequired": false, - "signature": [ - "string | undefined" - ], - "description": [ - "Another comment" - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 26, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L26" - } - } - ], - "tags": [], - "returnComment": [ - "something!" - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 21, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L21" - }, - "initialIsOpen": false - }, { "id": "def-public.arrowFn", "type": "Function", @@ -383,8 +234,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 42, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L42" + "lineNumber": 42 } }, { @@ -397,8 +247,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 43, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L43" + "lineNumber": 43 } }, { @@ -418,8 +267,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 44, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L44" + "lineNumber": 44 } }, { @@ -438,8 +286,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 45, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L45" + "lineNumber": 45 } }, { @@ -452,8 +299,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 46, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L46" + "lineNumber": 46 } } ], @@ -490,8 +336,7 @@ "label": "arrowFn", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 41, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L41" + "lineNumber": 41 }, "tags": [], "returnComment": [ @@ -518,15 +363,13 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 67, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67" + "lineNumber": 67 } } ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 67, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67" + "lineNumber": 67 } }, { @@ -544,8 +387,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 68, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68" + "lineNumber": 68 }, "signature": [ "(foo: { param: string; }) => number" @@ -554,8 +396,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 68, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68" + "lineNumber": 68 } }, { @@ -573,15 +414,13 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 69, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69" + "lineNumber": 69 } } ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 69, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69" + "lineNumber": 69 } } ], @@ -594,8 +433,7 @@ "label": "crazyFunction", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 66, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L66" + "lineNumber": 66 }, "tags": [], "returnComment": [ @@ -617,8 +455,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 76, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76" + "lineNumber": 76 } } ], @@ -629,102 +466,148 @@ "label": "fnWithNonExportedRef", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 76, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76" + "lineNumber": 76 }, "tags": [], "returnComment": [], "initialIsOpen": false - } - ], - "interfaces": [ + }, { - "id": "def-public.SearchSpec", - "type": "Interface", - "label": "SearchSpec", + "id": "def-public.notAnArrowFn", + "type": "Function", + "label": "notAnArrowFn", + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], "description": [ - "\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password." + "\nThis is a non arrow function.\n" ], - "tags": [], "children": [ { - "tags": [], - "id": "def-public.SearchSpec.username", "type": "string", - "label": "username", + "label": "a", + "isRequired": true, + "signature": [ + "string" + ], "description": [ - "\nStores the username. Duh," + "The letter A" ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 26, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L26" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 22 } }, { - "tags": [], - "id": "def-public.SearchSpec.password", - "type": "string", - "label": "password", + "type": "number", + "label": "b", + "isRequired": false, + "signature": [ + "number | undefined" + ], "description": [ - "\nStores the password. I hope it's encrypted!" + "Feed me to the function" ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 30, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L30" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 23 } - } - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 22, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L22" - }, - "initialIsOpen": false - }, - { - "id": "def-public.WithGen", - "type": "Interface", - "label": "WithGen", - "signature": [ + }, { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.WithGen", - "text": "WithGen" + "type": "Array", + "label": "c", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "So many params" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24 + } }, - "" - ], - "description": [ - "\nAn interface with a generic." - ], - "tags": [], - "children": [ { - "tags": [], - "id": "def-public.WithGen.t", - "type": "Uncategorized", - "label": "t", - "description": [], + "type": "CompoundType", + "label": "d", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "a great param" + ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 31, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L31" - }, + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25 + } + }, + { + "type": "string", + "label": "e", + "isRequired": false, "signature": [ - "T" - ] + "string | undefined" + ], + "description": [ + "Another comment" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26 + } } ], + "tags": [], + "returnComment": [ + "something!" + ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 30, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L30" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 21 }, "initialIsOpen": false - }, + } + ], + "interfaces": [ { "id": "def-public.AnotherInterface", "type": "Interface", @@ -750,8 +633,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 35, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L35" + "lineNumber": 35 }, "signature": [ "T" @@ -760,8 +642,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 34, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L34" + "lineNumber": 34 }, "initialIsOpen": false }, @@ -802,8 +683,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 75, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L75" + "lineNumber": 75 }, "signature": [ "() => Promise" @@ -819,8 +699,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 81, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L81" + "lineNumber": 81 }, "signature": [ "(t: T) => void" @@ -841,47 +720,13 @@ "returnComment": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 86, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L86" + "lineNumber": 86 } } ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 71, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L71" - }, - "initialIsOpen": false - }, - { - "id": "def-public.IReturnAReactComponent", - "type": "Interface", - "label": "IReturnAReactComponent", - "description": [ - "\nAn interface that has a react component." - ], - "tags": [], - "children": [ - { - "tags": [], - "id": "def-public.IReturnAReactComponent.component", - "type": "CompoundType", - "label": "component", - "description": [], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 93, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L93" - }, - "signature": [ - "React.ComponentType<{}>" - ] - } - ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", - "lineNumber": 92, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L92" + "lineNumber": 71 }, "initialIsOpen": false }, @@ -900,8 +745,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 44, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L44" + "lineNumber": 44 }, "signature": [ { @@ -916,143 +760,154 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 43, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L43" + "lineNumber": 43 }, "initialIsOpen": false - } - ], - "enums": [ + }, { - "id": "def-public.DayOfWeek", - "type": "Enum", - "label": "DayOfWeek", - "tags": [], + "id": "def-public.IReturnAReactComponent", + "type": "Interface", + "label": "IReturnAReactComponent", "description": [ - "\nComments on enums." + "\nAn interface that has a react component." ], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 31, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L31" - }, - "initialIsOpen": false - } - ], - "misc": [ - { "tags": [], - "id": "def-public.imAnAny", - "type": "Any", - "label": "imAnAny", - "description": [], + "children": [ + { + "tags": [], + "id": "def-public.IReturnAReactComponent.component", + "type": "CompoundType", + "label": "component", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 93 + }, + "signature": [ + "React.ComponentType<{}>" + ] + } + ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", - "lineNumber": 19, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L19" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 92 }, - "signature": [ - "any" - ], "initialIsOpen": false }, { + "id": "def-public.SearchSpec", + "type": "Interface", + "label": "SearchSpec", + "description": [ + "\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password." + ], "tags": [], - "id": "def-public.imAnUnknown", - "type": "Unknown", - "label": "imAnUnknown", - "description": [], + "children": [ + { + "tags": [], + "id": "def-public.SearchSpec.username", + "type": "string", + "label": "username", + "description": [ + "\nStores the username. Duh," + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 26 + } + }, + { + "tags": [], + "id": "def-public.SearchSpec.password", + "type": "string", + "label": "password", + "description": [ + "\nStores the password. I hope it's encrypted!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 30 + } + } + ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", - "lineNumber": 20, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L20" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 22 }, - "signature": [ - "unknown" - ], "initialIsOpen": false }, { - "id": "def-public.NotAnArrowFnType", - "type": "Type", - "label": "NotAnArrowFnType", - "tags": [], - "description": [], - "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", - "lineNumber": 78, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L78" - }, + "id": "def-public.WithGen", + "type": "Interface", + "label": "WithGen", "signature": [ - "(a: string, b: number | undefined, c: ", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.TypeWithGeneric", - "text": "TypeWithGeneric" - }, - ", d: ", { "pluginId": "pluginA", "scope": "public", "docId": "kibPluginAPluginApi", - "section": "def-public.ImAType", - "text": "ImAType" + "section": "def-public.WithGen", + "text": "WithGen" }, - ", e: string | undefined) => ", + "" + ], + "description": [ + "\nAn interface with a generic." + ], + "tags": [], + "children": [ { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.TypeWithGeneric", - "text": "TypeWithGeneric" - }, - "" + "tags": [], + "id": "def-public.WithGen.t", + "type": "Uncategorized", + "label": "t", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 31 + }, + "signature": [ + "T" + ] + } ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 30 + }, "initialIsOpen": false - }, + } + ], + "enums": [ { + "id": "def-public.DayOfWeek", + "type": "Enum", + "label": "DayOfWeek", "tags": [], - "id": "def-public.aUnionProperty", - "type": "CompoundType", - "label": "aUnionProperty", "description": [ - "\nThis is a complicated union type" + "\nComments on enums." ], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 58, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L58" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 31 }, - "signature": [ - "string | number | (() => string) | ", - { - "pluginId": "pluginA", - "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.CrazyClass", - "text": "CrazyClass" - }, - "" - ], "initialIsOpen": false - }, + } + ], + "misc": [ { "tags": [], - "id": "def-public.aStrArray", - "type": "Array", - "label": "aStrArray", + "id": "def-public.aNum", + "type": "number", + "label": "aNum", "description": [ - "\nThis is an array of strings. The type is explicit." + "\nIt's a number. A special number." ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 63, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L63" + "lineNumber": 78 }, "signature": [ - "string[]" + "10" ], "initialIsOpen": false }, @@ -1066,8 +921,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 68, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L68" + "lineNumber": 68 }, "signature": [ "number[]" @@ -1084,78 +938,104 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 73, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L73" + "lineNumber": 73 }, "initialIsOpen": false }, { "tags": [], - "id": "def-public.aNum", - "type": "number", - "label": "aNum", + "id": "def-public.aStrArray", + "type": "Array", + "label": "aStrArray", "description": [ - "\nIt's a number. A special number." + "\nThis is an array of strings. The type is explicit." ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 78, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L78" + "lineNumber": 63 }, "signature": [ - "10" + "string[]" ], "initialIsOpen": false }, { "tags": [], - "id": "def-public.literalString", - "type": "string", - "label": "literalString", + "id": "def-public.aUnionProperty", + "type": "CompoundType", + "label": "aUnionProperty", "description": [ - "\nI'm a type of string, but more specifically, a literal string type." + "\nThis is a complicated union type" ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 83, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L83" + "lineNumber": 58 }, "signature": [ - "\"HI\"" + "string | number | (() => string) | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.CrazyClass", + "text": "CrazyClass" + }, + "" ], "initialIsOpen": false }, { - "id": "def-public.StringOrUndefinedType", + "id": "def-public.FnWithGeneric", "type": "Type", - "label": "StringOrUndefinedType", + "label": "FnWithGeneric", "tags": [], "description": [ - "\nHow should a potentially undefined type show up." + "\nThis is a type that defines a function.\n" ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 15, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L15" + "lineNumber": 26 }, "signature": [ - "undefined | string" + "(t: T) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" ], "initialIsOpen": false }, { - "id": "def-public.TypeWithGeneric", - "type": "Type", - "label": "TypeWithGeneric", "tags": [], + "id": "def-public.imAnAny", + "type": "Any", + "label": "imAnAny", "description": [], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 17, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L17" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 19 }, "signature": [ - "T[]" + "any" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-public.imAnUnknown", + "type": "Unknown", + "label": "imAnUnknown", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 20 + }, + "signature": [ + "unknown" ], "initialIsOpen": false }, @@ -1167,8 +1047,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 19, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L19" + "lineNumber": 19 }, "signature": [ "string | number | ", @@ -1199,28 +1078,41 @@ "initialIsOpen": false }, { - "id": "def-public.FnWithGeneric", + "id": "def-public.IRefANotExportedType", "type": "Type", - "label": "FnWithGeneric", + "label": "IRefANotExportedType", "tags": [], - "description": [ - "\nThis is a type that defines a function.\n" - ], + "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 26, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L26" + "lineNumber": 42 }, "signature": [ - "(t: T) => ", { "pluginId": "pluginA", "scope": "public", - "docId": "kibPluginAPluginApi", - "section": "def-public.TypeWithGeneric", - "text": "TypeWithGeneric" + "docId": "kibPluginAFooPluginApi", + "section": "def-public.ImNotExportedFromIndex", + "text": "ImNotExportedFromIndex" }, - "" + " | { zed: \"hi\"; }" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-public.literalString", + "type": "string", + "label": "literalString", + "description": [ + "\nI'm a type of string, but more specifically, a literal string type." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 83 + }, + "signature": [ + "\"HI\"" ], "initialIsOpen": false }, @@ -1234,8 +1126,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 40, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L40" + "lineNumber": 40 }, "signature": [ "(typeof DayOfWeek)[]" @@ -1243,25 +1134,73 @@ "initialIsOpen": false }, { - "id": "def-public.IRefANotExportedType", + "id": "def-public.NotAnArrowFnType", "type": "Type", - "label": "IRefANotExportedType", + "label": "NotAnArrowFnType", "tags": [], "description": [], "source": { - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", - "lineNumber": 42, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L42" + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 78 }, "signature": [ + "(a: string, b: number | undefined, c: ", { "pluginId": "pluginA", "scope": "public", - "docId": "kibPluginAFooPluginApi", - "section": "def-public.ImNotExportedFromIndex", - "text": "ImNotExportedFromIndex" + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" }, - " | { zed: \"hi\"; }" + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.StringOrUndefinedType", + "type": "Type", + "label": "StringOrUndefinedType", + "tags": [], + "description": [ + "\nHow should a potentially undefined type show up." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 15 + }, + "signature": [ + "undefined | string" + ], + "initialIsOpen": false + }, + { + "id": "def-public.TypeWithGeneric", + "type": "Type", + "label": "TypeWithGeneric", + "tags": [], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 17 + }, + "signature": [ + "T[]" ], "initialIsOpen": false } @@ -1282,8 +1221,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 21, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L21" + "lineNumber": 21 }, "signature": [ "typeof ", @@ -1306,8 +1244,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 26, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L26" + "lineNumber": 26 }, "signature": [ "typeof ", @@ -1340,8 +1277,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 31, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31" + "lineNumber": 31 } } ], @@ -1369,8 +1305,7 @@ "label": "aPropertyInlineFn", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 31, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31" + "lineNumber": 31 }, "tags": [], "returnComment": [] @@ -1385,8 +1320,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 38, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L38" + "lineNumber": 38 } }, { @@ -1402,8 +1336,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 44, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L44" + "lineNumber": 44 } } ], @@ -1413,8 +1346,7 @@ "label": "nestedObj", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 43, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L43" + "lineNumber": 43 } } ], @@ -1424,8 +1356,7 @@ "label": "aPretendNamespaceObj", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", - "lineNumber": 17, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L17" + "lineNumber": 17 }, "initialIsOpen": false } @@ -1451,8 +1382,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 101, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L101" + "lineNumber": 101 }, "signature": [ "(searchSpec: ", @@ -1476,8 +1406,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 109, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L109" + "lineNumber": 109 }, "signature": [ "(searchSpec: { username: string; password: string; }) => string" @@ -1495,8 +1424,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 122, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L122" + "lineNumber": 122 }, "signature": [ "(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void" @@ -1512,8 +1440,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 133, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L133" + "lineNumber": 133 }, "signature": [ "(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }" @@ -1529,15 +1456,13 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 140, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L140" + "lineNumber": 140 } } ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 89, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L89" + "lineNumber": 89 }, "lifecycle": "setup", "initialIsOpen": true @@ -1559,8 +1484,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 68, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L68" + "lineNumber": 68 }, "signature": [ "() => ", @@ -1576,8 +1500,7 @@ ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", - "lineNumber": 64, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L64" + "lineNumber": 64 }, "lifecycle": "start", "initialIsOpen": true @@ -1610,15 +1533,13 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts", - "lineNumber": 12, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L12" + "lineNumber": 12 } } ], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts", - "lineNumber": 11, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L11" + "lineNumber": 11 }, "initialIsOpen": false } diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json index 00fb2bd3aa7a9..a529d1a36657b 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json @@ -14,8 +14,7 @@ "label": "doTheFooFnThing", "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", - "lineNumber": 9, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9" + "lineNumber": 9 }, "tags": [], "returnComment": [], @@ -33,8 +32,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", - "lineNumber": 11, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11" + "lineNumber": 11 }, "signature": [ "() => \"foo\"" @@ -66,8 +64,7 @@ "description": [], "source": { "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts", - "lineNumber": 9, - "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9" + "lineNumber": 9 }, "signature": [ "\"COMMON VAR!\"" diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index 2c36e24453c62..dbc455bbd2f8f 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; @@ -17,7 +17,7 @@ export async function emptyKibanaIndexAction({ log, kbnClient, }: { - client: Client; + client: KibanaClient; log: ToolingLog; kbnClient: KbnClient; }) { diff --git a/packages/kbn-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index 68d5437336023..248c4a65cb20a 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -11,7 +11,7 @@ import { createReadStream } from 'fs'; import { Readable } from 'stream'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { createPromiseFromStreams, concatStreamProviders } from '@kbn/utils'; import { ES_CLIENT_HEADERS } from '../client_headers'; @@ -48,7 +48,7 @@ export async function loadAction({ name: string; skipExisting: boolean; useCreate: boolean; - client: Client; + client: KibanaClient; dataDir: string; log: ToolingLog; kbnClient: KbnClient; diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 3790e0f013ee0..c90f241a1c639 100644 --- a/packages/kbn-es-archiver/src/actions/save.ts +++ b/packages/kbn-es-archiver/src/actions/save.ts @@ -9,7 +9,7 @@ import { resolve } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; import { Readable, Writable } from 'stream'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { createListStream, createPromiseFromStreams } from '@kbn/utils'; @@ -32,7 +32,7 @@ export async function saveAction({ }: { name: string; indices: string | string[]; - client: Client; + client: KibanaClient; dataDir: string; log: ToolingLog; raw: boolean; diff --git a/packages/kbn-es-archiver/src/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts index b5f259a1496bb..f4e37871a5337 100644 --- a/packages/kbn-es-archiver/src/actions/unload.ts +++ b/packages/kbn-es-archiver/src/actions/unload.ts @@ -9,7 +9,7 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; import { Readable, Writable } from 'stream'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; import { createPromiseFromStreams } from '@kbn/utils'; @@ -32,7 +32,7 @@ export async function unloadAction({ kbnClient, }: { name: string; - client: Client; + client: KibanaClient; dataDir: string; log: ToolingLog; kbnClient: KbnClient; diff --git a/packages/kbn-es-archiver/src/client_headers.ts b/packages/kbn-es-archiver/src/client_headers.ts index da240c3ad8318..5733eb9b97543 100644 --- a/packages/kbn-es-archiver/src/client_headers.ts +++ b/packages/kbn-es-archiver/src/client_headers.ts @@ -8,4 +8,4 @@ export const ES_CLIENT_HEADERS = { 'x-elastic-product-origin': 'kibana', -}; +} as const; diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 68eacb4f3caf2..93ce97efd4c84 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; @@ -20,14 +20,14 @@ import { } from './actions'; interface Options { - client: Client; + client: KibanaClient; dataDir: string; log: ToolingLog; kbnClient: KbnClient; } export class EsArchiver { - private readonly client: Client; + private readonly client: KibanaClient; private readonly dataDir: string; private readonly log: ToolingLog; private readonly kbnClient: KbnClient; diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts index cacd224e71421..88e167b3705cb 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts @@ -7,7 +7,7 @@ */ import { Transform } from 'stream'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { Stats } from '../stats'; import { Progress } from '../progress'; import { ES_CLIENT_HEADERS } from '../../client_headers'; @@ -21,7 +21,7 @@ export function createGenerateDocRecordsStream({ progress, query, }: { - client: Client; + client: KibanaClient; stats: Stats; progress: Progress; query?: Record; diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts index e105a243cae76..028ff16c9afb2 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import AggregateError from 'aggregate-error'; import { Writable } from 'stream'; import { Stats } from '../stats'; @@ -14,7 +14,7 @@ import { Progress } from '../progress'; import { ES_CLIENT_HEADERS } from '../../client_headers'; export function createIndexDocRecordsStream( - client: Client, + client: KibanaClient, stats: Stats, progress: Progress, useCreate: boolean = false diff --git a/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts b/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts index 59101f5490016..7dde4075dc3f2 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts +++ b/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import sinon from 'sinon'; import { ToolingLog } from '@kbn/dev-utils'; import { Stats } from '../../stats'; @@ -67,7 +67,7 @@ const createEsClientError = (errorType: string) => { const indexAlias = (aliases: Record, index: string) => Object.keys(aliases).find((k) => aliases[k] === index); -type StubClient = Client; +type StubClient = KibanaClient; export const createStubClient = ( existingIndices: string[] = [], diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts index 39e00ff0c72c0..28c8ccd1c28a8 100644 --- a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts @@ -125,7 +125,6 @@ describe('esArchiver: createCreateIndexStream()', () => { ]); sinon.assert.calledWith(client.indices.create as sinon.SinonSpy, { - method: 'PUT', index: 'index', body: { settings: undefined, diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts index ca89278305813..b45a8b18a5776 100644 --- a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts @@ -9,7 +9,8 @@ import { Transform, Readable } from 'stream'; import { inspect } from 'util'; -import { Client } from '@elastic/elasticsearch'; +import { estypes } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { Stats } from '../stats'; @@ -18,12 +19,9 @@ import { deleteIndex } from './delete_index'; import { ES_CLIENT_HEADERS } from '../../client_headers'; interface DocRecord { - value: { + value: estypes.IndexState & { index: string; type: string; - settings: Record; - mappings: Record; - aliases: Record; }; } @@ -33,7 +31,7 @@ export function createCreateIndexStream({ skipExisting = false, log, }: { - client: Client; + client: KibanaClient; stats: Stats; skipExisting?: boolean; log: ToolingLog; @@ -66,7 +64,6 @@ export function createCreateIndexStream({ await client.indices.create( { - method: 'PUT', index, body: { settings, diff --git a/packages/kbn-es-archiver/src/lib/indices/delete_index.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index.ts index b5641eec4b9da..2a42d52e2ca80 100644 --- a/packages/kbn-es-archiver/src/lib/indices/delete_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/delete_index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { Stats } from '../stats'; import { ES_CLIENT_HEADERS } from '../../client_headers'; @@ -15,7 +15,7 @@ import { ES_CLIENT_HEADERS } from '../../client_headers'; const PENDING_SNAPSHOT_STATUSES = ['INIT', 'STARTED', 'WAITING']; export async function deleteIndex(options: { - client: Client; + client: KibanaClient; stats: Stats; index: string | string[]; log: ToolingLog; @@ -84,7 +84,7 @@ export function isDeleteWhileSnapshotInProgressError(error: any) { * snapshotting this index to complete. */ export async function waitForSnapshotCompletion( - client: Client, + client: KibanaClient, index: string | string[], log: ToolingLog ) { diff --git a/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts index db065274a7b3b..e1552b5ed1e3b 100644 --- a/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts @@ -7,7 +7,7 @@ */ import { Transform } from 'stream'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { Stats } from '../stats'; @@ -15,7 +15,7 @@ import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; export function createDeleteIndexStream( - client: Client, + client: KibanaClient, stats: Stats, log: ToolingLog, kibanaPluginIds: string[] diff --git a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts index 4e0319c52264f..6619f1b3a601e 100644 --- a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts @@ -7,11 +7,11 @@ */ import { Transform } from 'stream'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { Stats } from '../stats'; import { ES_CLIENT_HEADERS } from '../../client_headers'; -export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { +export function createGenerateIndexRecordsStream(client: KibanaClient, stats: Stats) { return new Transform({ writableObjectMode: true, readableObjectMode: true, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index dc49085cbd458..fbef255cd9ee5 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -8,7 +8,7 @@ import { inspect } from 'util'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; import { Stats } from '../stats'; @@ -23,7 +23,7 @@ export async function deleteKibanaIndices({ stats, log, }: { - client: Client; + client: KibanaClient; stats: Stats; log: ToolingLog; }) { @@ -67,22 +67,27 @@ export async function migrateKibanaIndex(kbnClient: KbnClient) { * with .kibana, then filters out any that aren't actually Kibana's core * index (e.g. we don't want to remove .kibana_task_manager or the like). */ -async function fetchKibanaIndices(client: Client) { - const resp = await client.cat.indices( +function isKibanaIndex(index?: string): index is string { + return Boolean( + index && + (/^\.kibana(:?_\d*)?$/.test(index) || + /^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index)) + ); +} + +async function fetchKibanaIndices(client: KibanaClient) { + const resp = await client.cat.indices( { index: '.kibana*', format: 'json' }, { headers: ES_CLIENT_HEADERS, } ); - const isKibanaIndex = (index: string) => - /^\.kibana(:?_\d*)?$/.test(index) || - /^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index); if (!Array.isArray(resp.body)) { throw new Error(`expected response to be an array ${inspect(resp.body)}`); } - return resp.body.map((x: { index: string }) => x.index).filter(isKibanaIndex); + return resp.body.map((x: { index?: string }) => x.index).filter(isKibanaIndex); } const delay = (delayInMs: number) => new Promise((resolve) => setTimeout(resolve, delayInMs)); @@ -93,7 +98,7 @@ export async function cleanKibanaIndices({ log, kibanaPluginIds, }: { - client: Client; + client: KibanaClient; stats: Stats; log: ToolingLog; kibanaPluginIds: string[]; @@ -149,7 +154,13 @@ export async function cleanKibanaIndices({ stats.deletedIndex('.kibana'); } -export async function createDefaultSpace({ index, client }: { index: string; client: Client }) { +export async function createDefaultSpace({ + index, + client, +}: { + index: string; + client: KibanaClient; +}) { await client.create( { index, diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index ffc8d7ea8c505..c175979f0e820 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -101,7 +101,7 @@ OptimizerConfig { } `; -exports[`prepares assets for distribution: bar bundle 1`] = `"!function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}__webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{enumerable:!0,get:getter})},__webpack_require__.r=function(exports){\\"undefined\\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(exports,\\"__esModule\\",{value:!0})},__webpack_require__.t=function(value,mode){if(1&mode&&(value=__webpack_require__(value)),8&mode)return value;if(4&mode&&\\"object\\"==typeof value&&value&&value.__esModule)return value;var ns=Object.create(null);if(__webpack_require__.r(ns),Object.defineProperty(ns,\\"default\\",{enumerable:!0,value:value}),2&mode&&\\"string\\"!=typeof value)for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,\\"a\\",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p=\\"\\",__webpack_require__(__webpack_require__.s=3)}([function(module,exports,__webpack_require__){\\"use strict\\";var memo,isOldIE=function(){return void 0===memo&&(memo=Boolean(window&&document&&document.all&&!window.atob)),memo},getTarget=function(){var memo={};return function(target){if(void 0===memo[target]){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement)try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}memo[target]=styleTarget}return memo[target]}}(),stylesInDom=[];function getIndexByIdentifier(identifier){for(var result=-1,i=0;i/packages/kbn-server-http-tools'], +}; diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json new file mode 100644 index 0000000000000..a8f99689f3335 --- /dev/null +++ b/packages/kbn-server-http-tools/package.json @@ -0,0 +1,20 @@ +{ + "name": "@kbn/server-http-tools", + "main": "./target/index.js", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "rm -rf target && ../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@kbn/config-schema": "link:../kbn-config-schema", + "@kbn/crypto": "link:../kbn-crypto", + "@kbn/std": "link:../kbn-std" + }, + "devDependencies": { + "@kbn/utility-types": "link:../kbn-utility-types" + } +} \ No newline at end of file diff --git a/packages/kbn-server-http-tools/src/create_server.ts b/packages/kbn-server-http-tools/src/create_server.ts new file mode 100644 index 0000000000000..4752e342d5d3e --- /dev/null +++ b/packages/kbn-server-http-tools/src/create_server.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 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, ServerOptions } from '@hapi/hapi'; +import { ListenerOptions } from './get_listener_options'; + +export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { + const server = new Server(serverOptions); + + server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; + server.listener.setTimeout(listenerOptions.socketTimeout); + server.listener.on('timeout', (socket) => { + socket.destroy(); + }); + server.listener.on('clientError', (err, socket) => { + if (socket.writable) { + socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')); + } else { + socket.destroy(err); + } + }); + + return server; +} diff --git a/packages/kbn-server-http-tools/src/default_validation_error_handler.test.ts b/packages/kbn-server-http-tools/src/default_validation_error_handler.test.ts new file mode 100644 index 0000000000000..93b09ef13e030 --- /dev/null +++ b/packages/kbn-server-http-tools/src/default_validation_error_handler.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Request, ResponseToolkit } from '@hapi/hapi'; +import { + defaultValidationErrorHandler, + HapiValidationError, +} from './default_validation_error_handler'; + +const emptyOutput = { + statusCode: 400, + headers: {}, + payload: { + statusCode: 400, + error: '', + validation: { + source: '', + keys: [], + }, + }, +}; + +describe('defaultValidationErrorHandler', () => { + it('formats value validation errors correctly', () => { + expect.assertions(1); + const schema = Joi.array().items( + Joi.object({ + type: Joi.string().required(), + }).required() + ); + + const error = schema.validate([{}], { abortEarly: false }).error as HapiValidationError; + + // Emulate what Hapi v17 does by default + error.output = { ...emptyOutput }; + error.output.payload.validation.keys = ['0.type', '']; + + try { + defaultValidationErrorHandler({} as Request, {} as ResponseToolkit, error); + } catch (err) { + // Verify the empty string gets corrected to 'value' + expect(err.output.payload.validation.keys).toEqual(['0.type', 'value']); + } + }); +}); diff --git a/packages/kbn-server-http-tools/src/default_validation_error_handler.ts b/packages/kbn-server-http-tools/src/default_validation_error_handler.ts new file mode 100644 index 0000000000000..d2f4e993f3e4b --- /dev/null +++ b/packages/kbn-server-http-tools/src/default_validation_error_handler.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Lifecycle, Request, ResponseToolkit, Util } from '@hapi/hapi'; +import { ValidationError } from 'joi'; +import Hoek from '@hapi/hoek'; + +/** + * Hapi extends the ValidationError interface to add this output key with more data. + */ +export interface HapiValidationError extends ValidationError { + output: { + statusCode: number; + headers: Util.Dictionary; + payload: { + statusCode: number; + error: string; + message?: string; + validation: { + source: string; + keys: string[]; + }; + }; + }; +} + +/** + * Used to replicate Hapi v16 and below's validation responses. Should be used in the routes.validate.failAction key. + */ +export function defaultValidationErrorHandler( + request: Request, + h: ResponseToolkit, + err?: Error +): Lifecycle.ReturnValue { + // Newer versions of Joi don't format the key for missing params the same way. This shim + // provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class + // in JS so we have to rely on the `name` key before we can cast it. + // + // The Hapi code we're 'overwriting' can be found here: + // https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102 + if (err && err.name === 'ValidationError' && err.hasOwnProperty('output')) { + const validationError: HapiValidationError = err as HapiValidationError; + const validationKeys: string[] = []; + + validationError.details.forEach((detail) => { + if (detail.path.length > 0) { + validationKeys.push(Hoek.escapeHtml(detail.path.join('.'))); + } else { + // If no path, use the value sigil to signal the entire value had an issue. + validationKeys.push('value'); + } + }); + + validationError.output.payload.validation.keys = validationKeys; + } + + throw err; +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js b/packages/kbn-server-http-tools/src/get_listener_options.ts similarity index 50% rename from src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js rename to packages/kbn-server-http-tools/src/get_listener_options.ts index af8404eb6da92..00884312b599f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js +++ b/packages/kbn-server-http-tools/src/get_listener_options.ts @@ -6,12 +6,16 @@ * Side Public License, v 1. */ -import React, { useContext } from 'react'; -import { CoreStartContext } from '../contexts/query_input_bar_context'; -import { QueryStringInput } from '../../../../../plugins/data/public'; +import { IHttpConfig } from './types'; -export function QueryBarWrapper(props) { - const coreStartContext = useContext(CoreStartContext); +export interface ListenerOptions { + keepaliveTimeout: number; + socketTimeout: number; +} - return ; +export function getListenerOptions(config: IHttpConfig): ListenerOptions { + return { + keepaliveTimeout: config.keepaliveTimeout, + socketTimeout: config.socketTimeout, + }; } diff --git a/packages/kbn-server-http-tools/src/get_request_id.test.ts b/packages/kbn-server-http-tools/src/get_request_id.test.ts new file mode 100644 index 0000000000000..1b098ed4842d3 --- /dev/null +++ b/packages/kbn-server-http-tools/src/get_request_id.test.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 { getRequestId } from './get_request_id'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), +})); + +describe('getRequestId', () => { + describe('when allowFromAnyIp is true', () => { + it('generates a UUID if no x-opaque-id header is present', () => { + const request = { + headers: {}, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + + it('uses x-opaque-id header value if present', () => { + const request = { + headers: { + 'x-opaque-id': 'id from header', + }, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( + 'id from header' + ); + }); + }); + + describe('when allowFromAnyIp is false', () => { + describe('and ipAllowlist is empty', () => { + it('generates a UUID even if x-opaque-id header is present', () => { + const request = { + headers: { 'x-opaque-id': 'id from header' }, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + }); + + describe('and ipAllowlist is not empty', () => { + it('uses x-opaque-id header if request comes from trusted IP address', () => { + const request = { + headers: { 'x-opaque-id': 'id from header' }, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( + 'id from header' + ); + }); + + it('generates a UUID if request comes from untrusted IP address', () => { + const request = { + headers: { 'x-opaque-id': 'id from header' }, + raw: { req: { socket: { remoteAddress: '5.5.5.5' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + + it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => { + const request = { + headers: {}, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + }); + }); +}); diff --git a/packages/kbn-server-http-tools/src/get_request_id.ts b/packages/kbn-server-http-tools/src/get_request_id.ts new file mode 100644 index 0000000000000..3af70ecc3a7a3 --- /dev/null +++ b/packages/kbn-server-http-tools/src/get_request_id.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Request } from '@hapi/hapi'; +import uuid from 'uuid'; + +export function getRequestId( + request: Request, + { allowFromAnyIp, ipAllowlist }: { allowFromAnyIp: boolean; ipAllowlist: string[] } +): string { + const remoteAddress = request.raw.req.socket?.remoteAddress; + return allowFromAnyIp || + // socket may be undefined in integration tests that connect via the http listener directly + (remoteAddress && ipAllowlist.includes(remoteAddress)) + ? request.headers['x-opaque-id'] ?? uuid.v4() + : uuid.v4(); +} diff --git a/packages/kbn-server-http-tools/src/get_server_options.test.ts b/packages/kbn-server-http-tools/src/get_server_options.test.ts new file mode 100644 index 0000000000000..fdcc749f4ae9a --- /dev/null +++ b/packages/kbn-server-http-tools/src/get_server_options.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ByteSizeValue } from '@kbn/config-schema'; +import { getServerOptions } from './get_server_options'; +import { IHttpConfig } from './types'; + +jest.mock('fs', () => { + const original = jest.requireActual('fs'); + return { + // Hapi Inert patches native methods + ...original, + readFileSync: jest.fn(), + }; +}); + +const createConfig = (parts: Partial): IHttpConfig => ({ + host: 'localhost', + port: 5601, + socketTimeout: 120000, + keepaliveTimeout: 120000, + maxPayload: ByteSizeValue.parse('1048576b'), + ...parts, + cors: { + enabled: false, + allowCredentials: false, + allowOrigin: ['*'], + ...parts.cors, + }, + ssl: { + enabled: false, + ...parts.ssl, + }, +}); + +describe('getServerOptions', () => { + beforeEach(() => + jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`) + ); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('properly configures TLS with default options', () => { + const httpConfig = createConfig({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + }, + }); + + expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "cert": "some-certificate-path", + "ciphers": undefined, + "honorCipherOrder": true, + "key": "some-key-path", + "passphrase": undefined, + "rejectUnauthorized": undefined, + "requestCert": undefined, + "secureOptions": undefined, + } + `); + }); + + it('properly configures TLS with client authentication', () => { + const httpConfig = createConfig({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + certificateAuthorities: ['ca-1', 'ca-2'], + cipherSuites: ['suite-a', 'suite-b'], + keyPassphrase: 'passPhrase', + rejectUnauthorized: true, + requestCert: true, + getSecureOptions: () => 42, + }, + }); + + expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` + Object { + "ca": Array [ + "ca-1", + "ca-2", + ], + "cert": "some-certificate-path", + "ciphers": "suite-a:suite-b", + "honorCipherOrder": true, + "key": "some-key-path", + "passphrase": "passPhrase", + "rejectUnauthorized": true, + "requestCert": true, + "secureOptions": 42, + } + `); + }); + + it('properly configures CORS when cors enabled', () => { + const httpConfig = createConfig({ + cors: { + enabled: true, + allowCredentials: false, + allowOrigin: ['*'], + }, + }); + + expect(getServerOptions(httpConfig).routes?.cors).toEqual({ + credentials: false, + origin: ['*'], + headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'], + }); + }); +}); diff --git a/packages/kbn-server-http-tools/src/get_server_options.ts b/packages/kbn-server-http-tools/src/get_server_options.ts new file mode 100644 index 0000000000000..ade90a0e0d3f5 --- /dev/null +++ b/packages/kbn-server-http-tools/src/get_server_options.ts @@ -0,0 +1,75 @@ +/* + * Copyright 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 { RouteOptionsCors, ServerOptions } from '@hapi/hapi'; +import { ServerOptions as TLSOptions } from 'https'; +import { defaultValidationErrorHandler } from './default_validation_error_handler'; +import { IHttpConfig } from './types'; + +const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf']; + +/** + * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. + */ +export function getServerOptions(config: IHttpConfig, { configureTLS = true } = {}) { + const cors: RouteOptionsCors | false = config.cors.enabled + ? { + credentials: config.cors.allowCredentials, + origin: config.cors.allowOrigin, + headers: corsAllowedHeaders, + } + : false; + const options: ServerOptions = { + host: config.host, + port: config.port, + routes: { + cache: { + privacy: 'private', + otherwise: 'private, no-cache, no-store, must-revalidate', + }, + cors, + payload: { + maxBytes: config.maxPayload.getValueInBytes(), + }, + validate: { + failAction: defaultValidationErrorHandler, + options: { + abortEarly: false, + }, + }, + }, + state: { + strictHeader: false, + isHttpOnly: true, + isSameSite: false, // necessary to allow using Kibana inside an iframe + }, + }; + + if (configureTLS && config.ssl.enabled) { + const ssl = config.ssl; + + // TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of + // `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`. + const tlsOptions: TLSOptions = { + ca: ssl.certificateAuthorities, + cert: ssl.certificate, + ciphers: config.ssl.cipherSuites?.join(':'), + // We use the server's cipher order rather than the client's to prevent the BEAST attack. + honorCipherOrder: true, + key: ssl.key, + passphrase: ssl.keyPassphrase, + secureOptions: ssl.getSecureOptions ? ssl.getSecureOptions() : undefined, + requestCert: ssl.requestCert, + rejectUnauthorized: ssl.rejectUnauthorized, + }; + + options.tls = tlsOptions; + } + + return options; +} diff --git a/packages/kbn-server-http-tools/src/index.ts b/packages/kbn-server-http-tools/src/index.ts new file mode 100644 index 0000000000000..bd1dffa0bb0ca --- /dev/null +++ b/packages/kbn-server-http-tools/src/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 type { IHttpConfig, ISslConfig, ICorsConfig } from './types'; +export { createServer } from './create_server'; +export { defaultValidationErrorHandler } from './default_validation_error_handler'; +export { getListenerOptions } from './get_listener_options'; +export { getServerOptions } from './get_server_options'; +export { getRequestId } from './get_request_id'; +export { sslSchema, SslConfig } from './ssl'; diff --git a/src/core/server/legacy/cli_dev_mode.js b/packages/kbn-server-http-tools/src/ssl/index.ts similarity index 86% rename from src/core/server/legacy/cli_dev_mode.js rename to packages/kbn-server-http-tools/src/ssl/index.ts index 3c4bdb4149780..cbc3f17f915ef 100644 --- a/src/core/server/legacy/cli_dev_mode.js +++ b/packages/kbn-server-http-tools/src/ssl/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { CliDevMode } from '../../../dev/cli_dev_mode'; +export { SslConfig, sslSchema } from './ssl_config'; diff --git a/src/core/server/http/ssl_config.test.mocks.ts b/packages/kbn-server-http-tools/src/ssl/ssl_config.test.mocks.ts similarity index 95% rename from src/core/server/http/ssl_config.test.mocks.ts rename to packages/kbn-server-http-tools/src/ssl/ssl_config.test.mocks.ts index 81dbcf55100f8..adc4adb76f804 100644 --- a/src/core/server/http/ssl_config.test.mocks.ts +++ b/packages/kbn-server-http-tools/src/ssl/ssl_config.test.mocks.ts @@ -13,7 +13,7 @@ jest.mock('fs', () => { export const mockReadPkcs12Keystore = jest.fn(); export const mockReadPkcs12Truststore = jest.fn(); -jest.mock('../utils', () => ({ +jest.mock('@kbn/crypto', () => ({ readPkcs12Keystore: mockReadPkcs12Keystore, readPkcs12Truststore: mockReadPkcs12Truststore, })); diff --git a/src/core/server/http/ssl_config.test.ts b/packages/kbn-server-http-tools/src/ssl/ssl_config.test.ts similarity index 99% rename from src/core/server/http/ssl_config.test.ts rename to packages/kbn-server-http-tools/src/ssl/ssl_config.test.ts index bb6b1c7ff29f3..112fcd8a449f7 100644 --- a/src/core/server/http/ssl_config.test.ts +++ b/packages/kbn-server-http-tools/src/ssl/ssl_config.test.ts @@ -34,7 +34,7 @@ describe('#SslConfig', () => { beforeEach(() => { const realFs = jest.requireActual('fs'); mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); - const utils = jest.requireActual('../utils'); + const utils = jest.requireActual('@kbn/crypto'); mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => utils.readPkcs12Keystore(path, password) ); diff --git a/src/core/server/http/ssl_config.ts b/packages/kbn-server-http-tools/src/ssl/ssl_config.ts similarity index 93% rename from src/core/server/http/ssl_config.ts rename to packages/kbn-server-http-tools/src/ssl/ssl_config.ts index 917d416a77563..53d3616a09a75 100644 --- a/src/core/server/http/ssl_config.ts +++ b/packages/kbn-server-http-tools/src/ssl/ssl_config.ts @@ -7,9 +7,9 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { readPkcs12Keystore, readPkcs12Truststore } from '@kbn/crypto'; import { constants as cryptoConstants } from 'crypto'; import { readFileSync } from 'fs'; -import { readPkcs12Keystore, readPkcs12Truststore } from '../utils'; const protocolMap = new Map([ ['TLSv1', cryptoConstants.SSL_OP_NO_TLSv1], @@ -81,14 +81,13 @@ type SslConfigType = TypeOf; export class SslConfig { public enabled: boolean; - public redirectHttpFromPort: number | undefined; - public key: string | undefined; - public certificate: string | undefined; - public certificateAuthorities: string[] | undefined; - public keyPassphrase: string | undefined; + public redirectHttpFromPort?: number; + public key?: string; + public certificate?: string; + public certificateAuthorities?: string[]; + public keyPassphrase?: string; public requestCert: boolean; public rejectUnauthorized: boolean; - public cipherSuites: string[]; public supportedProtocols: string[]; @@ -164,6 +163,4 @@ export class SslConfig { } } -const readFile = (file: string) => { - return readFileSync(file, 'utf8'); -}; +const readFile = (file: string) => readFileSync(file, 'utf8'); diff --git a/packages/kbn-server-http-tools/src/types.ts b/packages/kbn-server-http-tools/src/types.ts new file mode 100644 index 0000000000000..3cc117d542eee --- /dev/null +++ b/packages/kbn-server-http-tools/src/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ByteSizeValue } from '@kbn/config-schema'; + +export interface IHttpConfig { + host: string; + port: number; + maxPayload: ByteSizeValue; + keepaliveTimeout: number; + socketTimeout: number; + cors: ICorsConfig; + ssl: ISslConfig; +} + +export interface ICorsConfig { + enabled: boolean; + allowCredentials: boolean; + allowOrigin: string[]; +} + +export interface ISslConfig { + enabled: boolean; + key?: string; + certificate?: string; + certificateAuthorities?: string[]; + cipherSuites?: string[]; + keyPassphrase?: string; + requestCert?: boolean; + rejectUnauthorized?: boolean; + getSecureOptions?: () => number; +} diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json new file mode 100644 index 0000000000000..ec84b963aed70 --- /dev/null +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*" + ], + "dependencies": { + "@kbn/std": "link:../kbn-std" + } +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 0694bc4ffdb0f..d82b7b83e8f15 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -13,8 +13,8 @@ import Joi from 'joi'; // valid pattern for ID // enforced camel-case identifiers for consistency const ID_PATTERN = /^[a-zA-Z0-9_]+$/; -const INSPECTING = - process.execArgv.includes('--inspect') || process.execArgv.includes('--inspect-brk'); +// it will search both --inspect and --inspect-brk +const INSPECTING = !!process.execArgv.find((arg) => arg.includes('--inspect')); const urlPartsSchema = () => Joi.object() diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js index 4abbc3d29fe7c..a43d3a09c7d70 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js @@ -62,15 +62,11 @@ function collectCliArgs(config, { installDir, extraKbnOpts }) { const buildArgs = config.get('kbnTestServer.buildArgs') || []; const sourceArgs = config.get('kbnTestServer.sourceArgs') || []; const serverArgs = config.get('kbnTestServer.serverArgs') || []; - const execArgv = process.execArgv || []; return pipe( serverArgs, (args) => (installDir ? args.filter((a) => a !== '--oss') : args), - (args) => - installDir - ? [...buildArgs, ...args] - : [...execArgv, KIBANA_EXEC_PATH, ...sourceArgs, ...args], + (args) => (installDir ? [...buildArgs, ...args] : [KIBANA_EXEC_PATH, ...sourceArgs, ...args]), (args) => args.concat(extraKbnOpts || []) ); } diff --git a/packages/kbn-test/src/jest/utils/enzyme_helpers.tsx b/packages/kbn-test/src/jest/utils/enzyme_helpers.tsx index f517565434c18..686a201761dcd 100644 --- a/packages/kbn-test/src/jest/utils/enzyme_helpers.tsx +++ b/packages/kbn-test/src/jest/utils/enzyme_helpers.tsx @@ -85,6 +85,7 @@ export function mountWithIntl( childContextTypes, ...props }: { + attachTo?: HTMLElement; context?: any; childContextTypes?: ValidationMap; } = {} diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index 43b6c90452b81..d472f27395ffb 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -12,9 +12,9 @@ import { get, toPath } from 'lodash'; import { Cluster } from '@kbn/es'; import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; import { esTestConfig } from './es_test_config'; +import { Client } from '@elastic/elasticsearch'; import { KIBANA_ROOT } from '../'; -import * as legacyElasticsearch from 'elasticsearch'; const path = require('path'); const del = require('del'); @@ -102,8 +102,8 @@ export function createLegacyEsTestCluster(options = {}) { * Returns an ES Client to the configured cluster */ getClient() { - return new legacyElasticsearch.Client({ - host: this.getUrl(), + return new Client({ + node: this.getUrl(), }); } diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 7a996e98762ce..135884fbf13e7 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -9,6 +9,9 @@ const Path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); + const CompressionPlugin = require('compression-webpack-plugin'); const { REPO_ROOT } = require('@kbn/utils'); const webpack = require('webpack'); @@ -105,6 +108,28 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ }, optimization: { + minimizer: [ + new CssMinimizerPlugin({ + minimizerOptions: { + preset: [ + 'default', + { + discardComments: false, + }, + ], + }, + }), + new TerserPlugin({ + cache: false, + sourceMap: false, + extractComments: false, + parallel: false, + terserOptions: { + compress: true, + mangle: true, + }, + }), + ], noEmitOnErrors: true, splitChunks: { cacheGroups: { diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index a8f6e25276cec..33419ee0f1ec4 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -6,7 +6,7 @@ "main": "target", "types": "target/index.d.ts", "kibana": { - "devOnly": true + "devOnly": false }, "scripts": { "build": "../../node_modules/.bin/tsc", diff --git a/packages/kbn-utils/src/repo_root.ts b/packages/kbn-utils/src/repo_root.ts index 20a25023f4166..2c1617098fe20 100644 --- a/packages/kbn-utils/src/repo_root.ts +++ b/packages/kbn-utils/src/repo_root.ts @@ -57,3 +57,5 @@ const { kibanaDir, kibanaPkgJson } = findKibanaPackageJson(); export const REPO_ROOT = kibanaDir; export const UPSTREAM_BRANCH = kibanaPkgJson.branch; + +export const fromRoot = (...paths: string[]) => Path.resolve(REPO_ROOT, ...paths); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 13c16691bf12a..86b4ac53841f7 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -12,10 +12,8 @@ import { statSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; -import { getConfigPath } from '@kbn/utils'; +import { getConfigPath, fromRoot } from '@kbn/utils'; import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; -import { fromRoot } from '../../core/server/utils'; -import { bootstrap } from '../../core/server'; import { readKeystore } from '../keystore/read_keystore'; function canRequire(path) { @@ -31,9 +29,21 @@ function canRequire(path) { } } -const DEV_MODE_PATH = resolve(__dirname, '../../dev/cli_dev_mode'); +const DEV_MODE_PATH = '@kbn/cli-dev-mode'; const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH); +const getBootstrapScript = (isDev) => { + if (DEV_MODE_SUPPORTED && isDev && process.env.isDevCliChild !== 'true') { + // need dynamic require to exclude it from production build + // eslint-disable-next-line import/no-dynamic-require + const { bootstrapDevMode } = require(DEV_MODE_PATH); + return bootstrapDevMode; + } else { + const { bootstrap } = require('../../core/server'); + return bootstrap; + } +}; + const pathCollector = function () { const paths = []; return function (path) { @@ -68,6 +78,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.ssl) { // @kbn/dev-utils is part of devDependencies + // eslint-disable-next-line import/no-extraneous-dependencies const { CA_CERT_PATH, KBN_KEY_PATH, KBN_CERT_PATH } = require('@kbn/dev-utils'); const customElasticsearchHosts = opts.elasticsearch ? opts.elasticsearch.split(',') @@ -78,6 +89,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { throw new Error(`Can't use --ssl when "${path}" configuration is already defined.`); } } + ensureNotDefined('server.ssl.certificate'); ensureNotDefined('server.ssl.key'); ensureNotDefined('server.ssl.keystore.path'); @@ -209,31 +221,40 @@ export default function (program) { } const unknownOptions = this.getUnknownOptions(); - await bootstrap({ - configs: [].concat(opts.config || []), - cliArgs: { - dev: !!opts.dev, - envName: unknownOptions.env ? unknownOptions.env.name : undefined, - // no longer supported - quiet: !!opts.quiet, - silent: !!opts.silent, - watch: !!opts.watch, - runExamples: !!opts.runExamples, - // We want to run without base path when the `--run-examples` flag is given so that we can use local - // links in other documentation sources, like "View this tutorial [here](http://localhost:5601/app/tutorial/xyz)". - // We can tell users they only have to run with `yarn start --run-examples` to get those - // local links to work. Similar to what we do for "View in Console" links in our - // elastic.co links. - basePath: opts.runExamples ? false : !!opts.basePath, - optimize: !!opts.optimize, - disableOptimizer: !opts.optimizer, - oss: !!opts.oss, - cache: !!opts.cache, - dist: !!opts.dist, - }, - features: { - isCliDevModeSupported: DEV_MODE_SUPPORTED, - }, + const configs = [].concat(opts.config || []); + const cliArgs = { + dev: !!opts.dev, + envName: unknownOptions.env ? unknownOptions.env.name : undefined, + // no longer supported + quiet: !!opts.quiet, + silent: !!opts.silent, + watch: !!opts.watch, + runExamples: !!opts.runExamples, + // We want to run without base path when the `--run-examples` flag is given so that we can use local + // links in other documentation sources, like "View this tutorial [here](http://localhost:5601/app/tutorial/xyz)". + // We can tell users they only have to run with `yarn start --run-examples` to get those + // local links to work. Similar to what we do for "View in Console" links in our + // elastic.co links. + basePath: opts.runExamples ? false : !!opts.basePath, + optimize: !!opts.optimize, + disableOptimizer: !opts.optimizer, + oss: !!opts.oss, + cache: !!opts.cache, + dist: !!opts.dist, + }; + + // In development mode, the main process uses the @kbn/dev-cli-mode + // bootstrap script instead of core's. The DevCliMode instance + // is in charge of starting up the optimizer, and spawning another + // `/script/kibana` process with the `isDevCliChild` varenv set to true. + // This variable is then used to identify that we're the 'real' + // Kibana server process, and will be using core's bootstrap script + // to effectively start Kibana. + const bootstrapScript = getBootstrapScript(cliArgs.dev); + + await bootstrapScript({ + configs, + cliArgs, applyConfigOverrides: (rawConfig) => applyConfigOverrides(rawConfig, opts, unknownOptions), }); }); diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 82a0419b1d0cf..00cc827a1e83f 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -4050,54 +4050,74 @@ exports[`Header renders 1`] = ` hasArrow={true} id="headerHelpMenu" isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="m" repositionOnScroll={true} > -

-
- - -
+ +
- + @@ -4194,26 +4214,81 @@ exports[`Header renders 1`] = ` data-test-subj="toggleNavButton" onClick={[Function]} > - , + } + } className="euiHeaderSectionItem__button" + color="text" data-test-subj="toggleNavButton" onClick={[Function]} - type="button" > - - - - + + + + + + + + + + + diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 278bbe469e862..b68a7ced118d2 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -28,6 +28,7 @@ import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; import { IntegrationsService } from './integrations'; +import { DeprecationsService } from './deprecations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; @@ -82,7 +83,7 @@ export class CoreSystem { private readonly rendering: RenderingService; private readonly integrations: IntegrationsService; private readonly coreApp: CoreApp; - + private readonly deprecations: DeprecationsService; private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; private fatalErrorsSetup: FatalErrorsSetup | null = null; @@ -113,6 +114,7 @@ export class CoreSystem { this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); + this.deprecations = new DeprecationsService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); @@ -195,6 +197,7 @@ export class CoreSystem { injectedMetadata, notifications, }); + const deprecations = this.deprecations.start({ http }); this.coreApp.start({ application, http, notifications, uiSettings }); @@ -210,6 +213,7 @@ export class CoreSystem { overlays, uiSettings, fatalErrors, + deprecations, }; await this.plugins.start(core); @@ -252,6 +256,7 @@ export class CoreSystem { this.chrome.stop(); this.i18n.stop(); this.application.stop(); + this.deprecations.stop(); this.rootDomElement.textContent = ''; } } diff --git a/src/core/public/deprecations/deprecations_client.test.ts b/src/core/public/deprecations/deprecations_client.test.ts new file mode 100644 index 0000000000000..2f52f7b4af195 --- /dev/null +++ b/src/core/public/deprecations/deprecations_client.test.ts @@ -0,0 +1,187 @@ +/* + * Copyright 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 { httpServiceMock } from '../http/http_service.mock'; +import { DeprecationsClient } from './deprecations_client'; +import type { DomainDeprecationDetails } from '../../server/types'; + +describe('DeprecationsClient', () => { + const http = httpServiceMock.createSetupContract(); + const mockDeprecations = [ + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-2' }, + ]; + + beforeEach(() => { + http.fetch.mockReset(); + http.fetch.mockResolvedValue({ deprecations: mockDeprecations }); + }); + + describe('getAllDeprecations', () => { + it('returns a list of deprecations', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const deprecations = await deprecationsClient.getAllDeprecations(); + expect(http.fetch).toBeCalledTimes(1); + expect(http.fetch).toBeCalledWith('/api/deprecations/', { + asSystemRequest: true, + }); + + expect(deprecations).toEqual(mockDeprecations); + }); + }); + + describe('getDeprecations', () => { + it('returns deprecations for a single domainId', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const deprecations = await deprecationsClient.getDeprecations('testPluginId-1'); + + expect(deprecations.length).toBe(2); + expect(deprecations).toEqual([ + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-1' }, + ]); + }); + + it('returns [] if the domainId does not have any deprecations', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const deprecations = await deprecationsClient.getDeprecations('testPluginId-4'); + + expect(deprecations).toEqual([]); + }); + + it('calls the fetch api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + http.fetch.mockResolvedValueOnce({ + deprecations: [{ domainId: 'testPluginId-1' }, { domainId: 'testPluginId-1' }], + }); + http.fetch.mockResolvedValueOnce({ + deprecations: [{ domainId: 'testPluginId-2' }, { domainId: 'testPluginId-2' }], + }); + const results = [ + ...(await deprecationsClient.getDeprecations('testPluginId-1')), + ...(await deprecationsClient.getDeprecations('testPluginId-2')), + ]; + + expect(http.fetch).toBeCalledTimes(2); + expect(results).toEqual([ + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-2' }, + { domainId: 'testPluginId-2' }, + ]); + }); + }); + + describe('isDeprecationResolvable', () => { + it('returns true if deprecation has correctiveActions.api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + }, + }, + }; + + const isResolvable = deprecationsClient.isDeprecationResolvable(mockDeprecationDetails); + + expect(isResolvable).toBe(true); + }); + + it('returns false if deprecation is missing correctiveActions.api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: {}, + }; + + const isResolvable = deprecationsClient.isDeprecationResolvable(mockDeprecationDetails); + + expect(isResolvable).toBe(false); + }); + }); + + describe('resolveDeprecation', () => { + it('fails if deprecation is not resolvable', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: {}, + }; + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(result).toEqual({ + status: 'fail', + reason: 'deprecation has no correctiveAction via api.', + }); + }); + + it('fetches the deprecation api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + }, + }, + }; + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(http.fetch).toBeCalledTimes(1); + expect(http.fetch).toBeCalledWith({ + path: 'some-path', + method: 'POST', + asSystemRequest: true, + body: JSON.stringify({ + extra_param: 123, + deprecationDetails: { domainId: 'testPluginId-1' }, + }), + }); + expect(result).toEqual({ status: 'ok' }); + }); + + it('fails when fetch fails', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockResponse = 'Failed to fetch'; + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + }, + }, + }; + http.fetch.mockRejectedValue({ body: { message: mockResponse } }); + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(result).toEqual({ status: 'fail', reason: mockResponse }); + }); + }); +}); diff --git a/src/core/public/deprecations/deprecations_client.ts b/src/core/public/deprecations/deprecations_client.ts new file mode 100644 index 0000000000000..e510ab1e79d17 --- /dev/null +++ b/src/core/public/deprecations/deprecations_client.ts @@ -0,0 +1,78 @@ +/* + * Copyright 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 { HttpStart } from '../http'; +import type { DomainDeprecationDetails, DeprecationsGetResponse } from '../../server/types'; + +/* @internal */ +export interface DeprecationsClientDeps { + http: Pick; +} + +/* @internal */ +export type ResolveDeprecationResponse = { status: 'ok' } | { status: 'fail'; reason: string }; + +export class DeprecationsClient { + private readonly http: Pick; + constructor({ http }: DeprecationsClientDeps) { + this.http = http; + } + + private fetchDeprecations = async (): Promise => { + const { deprecations } = await this.http.fetch('/api/deprecations/', { + asSystemRequest: true, + }); + + return deprecations; + }; + + public getAllDeprecations = async () => { + return await this.fetchDeprecations(); + }; + + public getDeprecations = async (domainId: string) => { + const deprecations = await this.fetchDeprecations(); + return deprecations.filter((deprecation) => deprecation.domainId === domainId); + }; + + public isDeprecationResolvable = (details: DomainDeprecationDetails) => { + return typeof details.correctiveActions.api === 'object'; + }; + + public resolveDeprecation = async ( + details: DomainDeprecationDetails + ): Promise => { + const { domainId, correctiveActions } = details; + // explicit check required for TS type guard + if (typeof correctiveActions.api !== 'object') { + return { + status: 'fail', + reason: 'deprecation has no correctiveAction via api.', + }; + } + + const { body, method, path } = correctiveActions.api; + try { + await this.http.fetch({ + path, + method, + asSystemRequest: true, + body: JSON.stringify({ + ...body, + deprecationDetails: { domainId }, + }), + }); + return { status: 'ok' }; + } catch (err) { + return { + status: 'fail', + reason: err.body.message, + }; + } + }; +} diff --git a/src/core/public/deprecations/deprecations_service.mock.ts b/src/core/public/deprecations/deprecations_service.mock.ts new file mode 100644 index 0000000000000..5bcd52982d513 --- /dev/null +++ b/src/core/public/deprecations/deprecations_service.mock.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 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { DeprecationsService } from './deprecations_service'; +import type { DeprecationsServiceStart } from './deprecations_service'; + +const createServiceMock = (): jest.Mocked => ({ + getAllDeprecations: jest.fn().mockResolvedValue([]), + getDeprecations: jest.fn().mockResolvedValue([]), + isDeprecationResolvable: jest.fn().mockReturnValue(false), + resolveDeprecation: jest.fn().mockResolvedValue({ status: 'ok', payload: {} }), +}); + +const createMock = () => { + const mocked: jest.Mocked> = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockReturnValue(void 0); + mocked.start.mockReturnValue(createServiceMock()); + return mocked; +}; + +export const deprecationsServiceMock = { + create: createMock, + createSetupContract: () => void 0, + createStartContract: createServiceMock, +}; diff --git a/src/core/public/deprecations/deprecations_service.ts b/src/core/public/deprecations/deprecations_service.ts new file mode 100644 index 0000000000000..d06e0071d2bc7 --- /dev/null +++ b/src/core/public/deprecations/deprecations_service.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 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 { CoreService } from '../../types'; +import type { HttpStart } from '../http'; +import { DeprecationsClient, ResolveDeprecationResponse } from './deprecations_client'; +import type { DomainDeprecationDetails } from '../../server/types'; + +/** + * DeprecationsService provides methods to fetch domain deprecation details from + * the Kibana server. + * + * @public + */ +export interface DeprecationsServiceStart { + /** + * Grabs deprecations details for all domains. + */ + getAllDeprecations: () => Promise; + /** + * Grabs deprecations for a specific domain. + * + * @param {string} domainId + */ + getDeprecations: (domainId: string) => Promise; + /** + * Returns a boolean if the provided deprecation can be automatically resolvable. + * + * @param {DomainDeprecationDetails} details + */ + isDeprecationResolvable: (details: DomainDeprecationDetails) => boolean; + /** + * Calls the correctiveActions.api to automatically resolve the depprecation. + * + * @param {DomainDeprecationDetails} details + */ + resolveDeprecation: (details: DomainDeprecationDetails) => Promise; +} + +export class DeprecationsService implements CoreService { + public setup(): void {} + + public start({ http }: { http: HttpStart }): DeprecationsServiceStart { + const deprecationsClient = new DeprecationsClient({ http }); + + return { + getAllDeprecations: deprecationsClient.getAllDeprecations, + getDeprecations: deprecationsClient.getDeprecations, + isDeprecationResolvable: deprecationsClient.isDeprecationResolvable, + resolveDeprecation: deprecationsClient.resolveDeprecation, + }; + } + + public stop(): void {} +} diff --git a/src/core/public/deprecations/index.ts b/src/core/public/deprecations/index.ts new file mode 100644 index 0000000000000..092cbed613ac2 --- /dev/null +++ b/src/core/public/deprecations/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { DeprecationsService } from './deprecations_service'; +export type { DeprecationsServiceStart } from './deprecations_service'; +export type { ResolveDeprecationResponse } from './deprecations_client'; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6279d62d2c40e..ef3172b620b23 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -108,7 +108,9 @@ export class DocLinksService { sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`, top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`, }, - runtimeFields: `${ELASTICSEARCH_DOCS}runtime.html`, + runtimeFields: { + mapping: `${ELASTICSEARCH_DOCS}runtime-mapping-fields.html`, + }, scriptedFields: { scriptFields: `${ELASTICSEARCH_DOCS}search-request-script-fields.html`, scriptAggs: `${ELASTICSEARCH_DOCS}search-aggregations.html`, @@ -191,6 +193,7 @@ export class DocLinksService { lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, maps: `${ELASTIC_WEBSITE_URL}maps`, + vega: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/vega.html`, }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, @@ -215,12 +218,15 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, }, monitoring: { - alertsCluster: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/cluster-alerts.html`, alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`, alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, + alertsKibanaThreadpoolRejections: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-thread-pool-rejections`, + alertsKibanaCCRReadExceptions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, + alertsKibanaLargeShardSize: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-large-shard-size`, + alertsKibanaClusterAlerts: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cluster-alerts`, metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, @@ -231,7 +237,7 @@ export class DocLinksService { apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, clusterPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-cluster`, elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, - elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}get-started-enable-security.html`, + elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, kibanaTLS: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`, kibanaPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-privileges.html`, @@ -284,6 +290,7 @@ export class DocLinksService { registerSourceOnly: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-source-only-repository`, registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, + restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, }, ingest: { pipelines: `${ELASTICSEARCH_DOCS}ingest.html`, @@ -379,7 +386,9 @@ export interface DocLinksStart { readonly sum: string; readonly top_hits: string; }; - readonly runtimeFields: string; + readonly runtimeFields: { + readonly mapping: string; + }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index c7b4c370eb6d7..750f2e27dc950 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -65,6 +65,7 @@ import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; +import { DeprecationsServiceStart } from './deprecations'; export type { PackageInfo, EnvironmentMode, IExternalUrlPolicy } from '../server/types'; export type { CoreContext, CoreSystem } from './core_system'; @@ -184,6 +185,8 @@ export type { ErrorToastOptions, } from './notifications'; +export type { DeprecationsServiceStart, ResolveDeprecationResponse } from './deprecations'; + export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; export { URL_MAX_LENGTH } from './core_app'; @@ -268,6 +271,8 @@ export interface CoreStart { uiSettings: IUiSettingsClient; /** {@link FatalErrorsStart} */ fatalErrors: FatalErrorsStart; + /** {@link DeprecationsServiceStart} */ + deprecations: DeprecationsServiceStart; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e47de84ea12b2..bd7623beba651 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -24,6 +24,7 @@ import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; +import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -37,6 +38,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; +export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; function createCoreSetupMock({ basePath = '', @@ -57,6 +59,7 @@ function createCoreSetupMock({ http: httpServiceMock.createSetupContract({ basePath }), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + deprecations: deprecationsServiceMock.createSetupContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, }, @@ -76,6 +79,7 @@ function createCoreStartMock({ basePath = '' } = {}) { overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), + deprecations: deprecationsServiceMock.createStartContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index 7a1f936fe7f39..0d10ac47d0b75 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -26,7 +26,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index d52cc090d5d19..19ebb5a9113c3 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -29,7 +29,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ @@ -49,7 +49,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ @@ -131,7 +131,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index b59516fa121fb..49c895aa80fc4 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -139,5 +139,6 @@ export function createPluginStartContext< getInjectedVar: deps.injectedMetadata.getInjectedVar, }, fatalErrors: deps.fatalErrors, + deprecations: deps.deprecations, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index e70b78f237d75..d7114f14e2f00 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -34,6 +34,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; +import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; export let mockPluginInitializers: Map; @@ -101,6 +102,7 @@ describe('PluginsService', () => { uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), + deprecations: deprecationsServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 396bf16cbdc6f..0a1c7a9b0fa36 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -11,6 +11,7 @@ import { ConfigDeprecationProvider } from '@kbn/config'; import { ConfigPath } from '@kbn/config'; import { DetailedPeerCertificate } from 'tls'; import { EnvironmentMode } from '@kbn/config'; +import { estypes } from '@elastic/elasticsearch'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; @@ -431,6 +432,8 @@ export interface CoreStart { // (undocumented) chrome: ChromeStart; // (undocumented) + deprecations: DeprecationsServiceStart; + // (undocumented) docLinks: DocLinksStart; // (undocumented) fatalErrors: FatalErrorsStart; @@ -471,6 +474,15 @@ export class CoreSystem { // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Record; +// @public +export interface DeprecationsServiceStart { + // Warning: (ae-forgotten-export) The symbol "DomainDeprecationDetails" needs to be exported by the entry point index.d.ts + getAllDeprecations: () => Promise; + getDeprecations: (domainId: string) => Promise; + isDeprecationResolvable: (details: DomainDeprecationDetails) => boolean; + resolveDeprecation: (details: DomainDeprecationDetails) => Promise; +} + // @public (undocumented) export interface DocLinksStart { // (undocumented) @@ -558,7 +570,9 @@ export interface DocLinksStart { readonly sum: string; readonly top_hits: string; }; - readonly runtimeFields: string; + readonly runtimeFields: { + readonly mapping: string; + }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; @@ -1072,6 +1086,16 @@ export type PublicAppSearchDeepLinkInfo = Omit; +// Warning: (ae-missing-release-tag) "ResolveDeprecationResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ResolveDeprecationResponse = { + status: 'ok'; +} | { + status: 'fail'; + reason: string; +}; + // Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1225,12 +1249,12 @@ export interface SavedObjectsFindOptions { preference?: string; rootSearchFields?: string[]; search?: string; - searchAfter?: unknown[]; + searchAfter?: estypes.Id[]; searchFields?: string[]; // (undocumented) sortField?: string; // (undocumented) - sortOrder?: string; + sortOrder?: estypes.SortOrder; // (undocumented) type: string | string[]; typeToNamespacesMap?: Map; @@ -1583,6 +1607,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:164:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 42f6d9aedf1d6..4a07e0c010685 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -11,18 +11,10 @@ import { CliArgs, Env, RawConfigService } from './config'; import { Root } from './root'; import { CriticalError } from './errors'; -interface KibanaFeatures { - // Indicates whether we can run Kibana in dev mode in which Kibana is run as - // a child process together with optimizer "worker" processes that are - // orchestrated by a parent process (dev mode only feature). - isCliDevModeSupported: boolean; -} - interface BootstrapArgs { configs: string[]; cliArgs: CliArgs; applyConfigOverrides: (config: Record) => Record; - features: KibanaFeatures; } /** @@ -30,12 +22,7 @@ interface BootstrapArgs { * @internal * @param param0 - options */ -export async function bootstrap({ - configs, - cliArgs, - applyConfigOverrides, - features, -}: BootstrapArgs) { +export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs) { if (cliArgs.optimize) { // --optimize is deprecated and does nothing now, avoid starting up and just shutdown return; @@ -52,7 +39,6 @@ export async function bootstrap({ const env = Env.createDefault(REPO_ROOT, { configs, cliArgs, - isDevCliParent: cliArgs.dev && features.isCliDevModeSupported && !process.env.isDevCliChild, }); const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides); diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index b6b3ab5b8face..e3c236405a596 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -20,7 +20,7 @@ const applyCoreDeprecations = (settings: Record = {}) => { deprecation, path: '', })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, @@ -305,7 +305,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -315,7 +315,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -361,7 +361,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -446,7 +446,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.filter\\" has been deprecated and will be removed in 8.0. ", + "\\"logging.filter\\" has been deprecated and will be removed in 8.0.", ] `); }); @@ -457,7 +457,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.filter\\" has been deprecated and will be removed in 8.0. ", + "\\"logging.filter\\" has been deprecated and will be removed in 8.0.", ] `); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 565b957b2a8e1..2e77374e3068a 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -9,40 +9,43 @@ import { has, get } from 'lodash'; import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config'; -const configPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const configPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(process.env, 'CONFIG_PATH')) { - log( - `Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder` - ); + addDeprecation({ + message: `Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder`, + }); } return settings; }; -const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(process.env, 'DATA_PATH')) { - log( - `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"` - ); + addDeprecation({ + message: `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`, + }); } return settings; }; -const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { - log( - 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + + addDeprecation({ + message: + 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + - 'current behavior and silence this warning.' - ); + 'current behavior and silence this warning.', + }); } return settings; }; -const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, log) => { +const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecation) => { const corsSettings = get(settings, 'server.cors'); if (typeof get(settings, 'server.cors') === 'boolean') { - log('"server.cors" is deprecated and has been replaced by "server.cors.enabled"'); + addDeprecation({ + message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"', + }); settings.server.cors = { enabled: corsSettings, }; @@ -50,7 +53,7 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; -const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { const NONCE_STRING = `{nonce}`; // Policies that should include the 'self' source const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); @@ -67,7 +70,9 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { settings.csp.rules = [...parsed].map(([policy, sourceList]) => { if (sourceList.find((source) => source.includes(NONCE_STRING))) { - log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); + addDeprecation({ + message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, + }); sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING)); // Add 'self' if not present @@ -80,7 +85,9 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { SELF_POLICIES.includes(policy) && !sourceList.find((source) => source.includes(SELF_STRING)) ) { - log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); + addDeprecation({ + message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, + }); sourceList.push(SELF_STRING); } @@ -91,149 +98,191 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; -const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( + settings, + fromPath, + addDeprecation +) => { if (has(settings, 'map.manifestServiceUrl')) { - log( - 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + + addDeprecation({ + message: + 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + 'of the Elastic Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' + '"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' + - 'modified for use in production environments.' - ); + 'modified for use in production environments.', + }); } return settings; }; -const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.ops')) { - log( - '"logging.events.ops" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.ops" has been deprecated and will be removed ' + 'in 8.0. To access ops data moving forward, please enable debug logs for the ' + '"metrics.ops" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.request') || has(settings, 'logging.events.response')) { - log( - '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' + 'in 8.0. To access request and/or response data moving forward, please enable debug logs for the ' + '"http.server.response" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.timezone')) { - log( - '"logging.timezone" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingtimezone', + message: + '"logging.timezone" has been deprecated and will be removed ' + 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.dest')) { - log( - '"logging.dest" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingdest', + message: + '"logging.dest" has been deprecated and will be removed ' + 'in 8.0. To set the destination moving forward, you can use the "console" appender ' + 'in your logging configuration or define a custom one. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.quiet')) { - log( - '"logging.quiet" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingquiet', + message: + '"logging.quiet" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ', + }); } return settings; }; -const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.silent')) { - log( - '"logging.silent" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingsilent', + message: + '"logging.silent" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ', + }); } return settings; }; -const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.verbose')) { - log( - '"logging.verbose" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingverbose', + message: + '"logging.verbose" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ', + }); } return settings; }; -const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { // We silence the deprecation warning when running in development mode because // the dev CLI code in src/dev/cli_dev_mode/using_server_process.ts manually // specifies `--logging.json=false`. Since it's executed in a child process, the // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY` if (has(settings, 'logging.json') && settings.env !== 'development') { - log( - '"logging.json" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + message: + '"logging.json" has been deprecated and will be removed ' + 'in 8.0. To specify log message format moving forward, ' + 'you can configure the "appender.layout" property for every custom appender in your logging configuration. ' + 'There is currently no default layout for custom appenders and each one must be declared explicitly. ' + 'For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.rotate')) { - log( - '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', + message: + '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' + 'Moving forward, you can enable log rotation using the "rolling-file" appender for a logger ' + 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', + }); } return settings; }; -const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.log')) { - log( - '"logging.events.log" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.log" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ', + }); } return settings; }; -const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.error')) { - log( - '"logging.events.error" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.error" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ', + }); } return settings; }; -const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.filter')) { - log('"logging.filter" has been deprecated and will be removed ' + 'in 8.0. '); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingfilter', + message: '"logging.filter" has been deprecated and will be removed in 8.0.', + }); } return settings; }; diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index bf2ce16e869b7..b1086d4470335 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -24,7 +24,7 @@ export type { ConfigPath, CliArgs, ConfigDeprecation, - ConfigDeprecationLogger, + AddConfigDeprecation, ConfigDeprecationProvider, ConfigDeprecationFactory, EnvironmentMode, diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index dfd0a9efc90c1..1c28eca1f1dec 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -120,10 +120,10 @@ describe('CoreUsageDataService', () => { body: [ { name: '.kibana_task_manager_1', - 'docs.count': 10, - 'docs.deleted': 10, - 'store.size': 1000, - 'pri.store.size': 2000, + 'docs.count': '10', + 'docs.deleted': '10', + 'store.size': '1000', + 'pri.store.size': '2000', }, ], } as any); @@ -131,10 +131,10 @@ describe('CoreUsageDataService', () => { body: [ { name: '.kibana_1', - 'docs.count': 20, - 'docs.deleted': 20, - 'store.size': 2000, - 'pri.store.size': 4000, + 'docs.count': '20', + 'docs.deleted': '20', + 'store.size': '2000', + 'pri.store.size': '4000', }, ], } as any); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index b9d8c9fc7e39f..dff68bf1c524f 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -118,10 +118,14 @@ export class CoreUsageDataService implements CoreService { + const logger = loggerMock.create(); + beforeEach(() => { + loggerMock.clear(logger); + }); + + describe('getRegistry', () => { + const domainId = 'test-plugin'; + + it('creates a registry for a domainId', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const registry = deprecationsFactory.getRegistry(domainId); + + expect(registry).toHaveProperty('registerDeprecations'); + expect(registry).toHaveProperty('getDeprecations'); + }); + + it('creates one registry for a domainId', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const registry = deprecationsFactory.getRegistry(domainId); + const sameRegistry = deprecationsFactory.getRegistry(domainId); + + expect(registry).toStrictEqual(sameRegistry); + }); + + it('returns a registered registry', () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const mockRegistry = 'mock-reg'; + const mockRegistries = { + set: jest.fn(), + get: jest.fn().mockReturnValue(mockRegistry), + }; + + // @ts-expect-error + deprecationsFactory.registries = mockRegistries; + const result = deprecationsFactory.getRegistry(domainId); + + expect(mockRegistries.get).toBeCalledTimes(1); + expect(mockRegistries.get).toBeCalledWith(domainId); + expect(mockRegistries.set).toBeCalledTimes(0); + expect(result).toStrictEqual(mockRegistry); + }); + }); + + describe('getAllDeprecations', () => { + const mockDependencies = ({ + esClient: jest.fn(), + savedObjectsClient: jest.fn(), + } as unknown) as GetDeprecationsContext; + + it('returns a flattened array of deprecations', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const mockPluginDeprecationsInfo = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + { + message: 'hello there!', + level: 'warning', + correctiveActions: { + manualSteps: ['mockPlugin step a', 'mockPlugin step b'], + }, + }, + ]; + const anotherMockPluginDeprecationsInfo = [ + { + message: 'anotherMockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['anotherMockPlugin step 1', 'anotherMockPlugin step 2'], + }, + }, + ]; + + const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const anotherMockPluginRegistry = deprecationsFactory.getRegistry('anotherMockPlugin'); + mockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(mockPluginDeprecationsInfo), + }); + anotherMockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(anotherMockPluginDeprecationsInfo), + }); + + const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + expect(derpecations).toStrictEqual( + [ + mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), + anotherMockPluginDeprecationsInfo.map((info) => ({ + ...info, + domainId: 'anotherMockPlugin', + })), + ].flat() + ); + }); + + it(`returns a failure message for failed getDeprecations functions`, async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const domainId = 'mockPlugin'; + const mockError = new Error(); + + const deprecationsRegistry = deprecationsFactory.getRegistry(domainId); + deprecationsRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockRejectedValue(mockError), + }); + const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + expect(logger.warn).toBeCalledTimes(1); + expect(logger.warn).toBeCalledWith( + `Failed to get deprecations info for plugin "${domainId}".`, + mockError + ); + expect(derpecations).toStrictEqual([ + { + domainId, + message: `Failed to get deprecations info for plugin "${domainId}".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]); + }); + + it(`returns successful results even when some getDeprecations fail`, async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const anotherMockPluginRegistry = deprecationsFactory.getRegistry('anotherMockPlugin'); + const mockError = new Error(); + const mockPluginDeprecationsInfo = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + ]; + mockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(mockPluginDeprecationsInfo), + }); + anotherMockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockRejectedValue(mockError), + }); + const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + + expect(logger.warn).toBeCalledTimes(1); + expect(logger.warn).toBeCalledWith( + `Failed to get deprecations info for plugin "anotherMockPlugin".`, + mockError + ); + expect(derpecations).toStrictEqual([ + ...mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), + { + domainId: 'anotherMockPlugin', + message: `Failed to get deprecations info for plugin "anotherMockPlugin".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]); + }); + }); + + describe('getDeprecations', () => { + const mockDependencies = ({ + esClient: jest.fn(), + savedObjectsClient: jest.fn(), + } as unknown) as GetDeprecationsContext; + + it('returns a flattened array of DeprecationInfo', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const deprecationsBody = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + [ + { + message: 'hello there!', + level: 'warning', + correctiveActions: { + manualSteps: ['mockPlugin step a', 'mockPlugin step b'], + }, + }, + ], + ]; + + deprecationsRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(deprecationsBody), + }); + + const derpecations = await deprecationsFactory.getDeprecations( + 'mockPlugin', + mockDependencies + ); + expect(derpecations).toStrictEqual( + deprecationsBody.flat().map((body) => ({ ...body, domainId: 'mockPlugin' })) + ); + }); + + it('removes empty entries from the returned array', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const deprecationsBody = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + [undefined], + undefined, + ]; + + deprecationsRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(deprecationsBody), + }); + + const derpecations = await deprecationsFactory.getDeprecations( + 'mockPlugin', + mockDependencies + ); + expect(derpecations).toHaveLength(1); + expect(derpecations).toStrictEqual([{ ...deprecationsBody[0], domainId: 'mockPlugin' }]); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_factory.ts b/src/core/server/deprecations/deprecations_factory.ts new file mode 100644 index 0000000000000..3699c088e20f1 --- /dev/null +++ b/src/core/server/deprecations/deprecations_factory.ts @@ -0,0 +1,108 @@ +/* + * Copyright 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 { DeprecationsRegistry } from './deprecations_registry'; +import type { Logger } from '../logging'; +import type { + DomainDeprecationDetails, + DeprecationsDetails, + GetDeprecationsContext, +} from './types'; + +export interface DeprecationsFactoryDeps { + logger: Logger; +} + +export class DeprecationsFactory { + private readonly registries: Map = new Map(); + private readonly logger: Logger; + constructor({ logger }: DeprecationsFactoryDeps) { + this.logger = logger; + } + + public getRegistry = (domainId: string): DeprecationsRegistry => { + const existing = this.registries.get(domainId); + if (existing) { + return existing; + } + const registry = new DeprecationsRegistry(); + this.registries.set(domainId, registry); + return registry; + }; + + public getDeprecations = async ( + domainId: string, + dependencies: GetDeprecationsContext + ): Promise => { + const infoBody = await this.getDeprecationsBody(domainId, dependencies); + return this.createDeprecationInfo(domainId, infoBody).flat(); + }; + + public getAllDeprecations = async ( + dependencies: GetDeprecationsContext + ): Promise => { + const domainIds = [...this.registries.keys()]; + + const deprecationsInfo = await Promise.all( + domainIds.map(async (domainId) => { + const infoBody = await this.getDeprecationsBody(domainId, dependencies); + return this.createDeprecationInfo(domainId, infoBody); + }) + ); + + return deprecationsInfo.flat(); + }; + + private createDeprecationInfo = ( + domainId: string, + deprecationInfoBody: DeprecationsDetails[] + ): DomainDeprecationDetails[] => { + return deprecationInfoBody + .flat() + .filter(Boolean) + .map((pluginDeprecation) => ({ + ...pluginDeprecation, + domainId, + })); + }; + + private getDeprecationsBody = async ( + domainId: string, + dependencies: GetDeprecationsContext + ): Promise => { + const deprecationsRegistry = this.registries.get(domainId); + if (!deprecationsRegistry) { + return []; + } + try { + const settledResults = await deprecationsRegistry.getDeprecations(dependencies); + return settledResults.flatMap((settledResult) => { + if (settledResult.status === 'rejected') { + this.logger.warn( + `Failed to get deprecations info for plugin "${domainId}".`, + settledResult.reason + ); + return [ + { + message: `Failed to get deprecations info for plugin "${domainId}".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]; + } + + return settledResult.value; + }); + } catch (err) { + this.logger.warn(`Failed to get deprecations info for plugin "${domainId}".`, err); + return []; + } + }; +} diff --git a/src/core/server/deprecations/deprecations_registry.test.ts b/src/core/server/deprecations/deprecations_registry.test.ts new file mode 100644 index 0000000000000..507677a531861 --- /dev/null +++ b/src/core/server/deprecations/deprecations_registry.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 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. + */ + +/* eslint-disable dot-notation */ +import { RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; +import { DeprecationsRegistry } from './deprecations_registry'; + +describe('DeprecationsRegistry', () => { + describe('registerDeprecations', () => { + it('throws if getDeprecations is not a function', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const deprecationsConfig = ({ + getDeprecations: null, + } as unknown) as RegisterDeprecationsConfig; + expect(() => deprecationsRegistry.registerDeprecations(deprecationsConfig)).toThrowError( + /getDeprecations must be a function/ + ); + }); + + it('registers deprecation context', () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const getDeprecations = jest.fn(); + const deprecationsConfig = { getDeprecations }; + deprecationsRegistry.registerDeprecations(deprecationsConfig); + expect(deprecationsRegistry['deprecationContexts']).toStrictEqual([deprecationsConfig]); + }); + + it('allows registering multiple contexts', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const deprecationsConfigA = { getDeprecations: jest.fn() }; + const deprecationsConfigB = { getDeprecations: jest.fn() }; + deprecationsRegistry.registerDeprecations(deprecationsConfigA); + deprecationsRegistry.registerDeprecations(deprecationsConfigB); + expect(deprecationsRegistry['deprecationContexts']).toStrictEqual([ + deprecationsConfigA, + deprecationsConfigB, + ]); + }); + }); + + describe('getDeprecations', () => { + it('returns all settled deprecations', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const mockContext = ({} as unknown) as GetDeprecationsContext; + const mockError = new Error(); + const deprecationsConfigA = { getDeprecations: jest.fn().mockResolvedValue('hi') }; + const deprecationsConfigB = { getDeprecations: jest.fn().mockRejectedValue(mockError) }; + deprecationsRegistry.registerDeprecations(deprecationsConfigA); + deprecationsRegistry.registerDeprecations(deprecationsConfigB); + const deprecations = await deprecationsRegistry.getDeprecations(mockContext); + expect(deprecations).toStrictEqual([ + { + status: 'fulfilled', + value: 'hi', + }, + { + status: 'rejected', + reason: mockError, + }, + ]); + }); + + it('passes dependencies to registered getDeprecations function', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const mockContext = ({} as unknown) as GetDeprecationsContext; + const deprecationsConfig = { getDeprecations: jest.fn().mockResolvedValue('hi') }; + deprecationsRegistry.registerDeprecations(deprecationsConfig); + const deprecations = await deprecationsRegistry.getDeprecations(mockContext); + expect(deprecations).toHaveLength(1); + expect(deprecationsConfig.getDeprecations).toBeCalledWith(mockContext); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_registry.ts b/src/core/server/deprecations/deprecations_registry.ts new file mode 100644 index 0000000000000..f92d807514b82 --- /dev/null +++ b/src/core/server/deprecations/deprecations_registry.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { DeprecationsDetails, RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; + +export class DeprecationsRegistry { + private readonly deprecationContexts: RegisterDeprecationsConfig[] = []; + + public registerDeprecations = (deprecationContext: RegisterDeprecationsConfig) => { + if (typeof deprecationContext.getDeprecations !== 'function') { + throw new Error(`getDeprecations must be a function in registerDeprecations(context)`); + } + + this.deprecationContexts.push(deprecationContext); + }; + + public getDeprecations = async ( + dependencies: GetDeprecationsContext + ): Promise>> => { + return await Promise.allSettled( + this.deprecationContexts.map( + async (deprecationContext) => await deprecationContext.getDeprecations(dependencies) + ) + ); + }; +} diff --git a/src/core/server/deprecations/deprecations_service.mock.ts b/src/core/server/deprecations/deprecations_service.mock.ts new file mode 100644 index 0000000000000..c0febf90a489a --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.mock.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 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { + DeprecationsService, + InternalDeprecationsServiceSetup, + DeprecationsServiceSetup, +} from './deprecations_service'; +type DeprecationsServiceContract = PublicMethodsOf; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + registerDeprecations: jest.fn(), + }; + + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const internalSetupContract: jest.Mocked = { + getRegistry: jest.fn(), + }; + + internalSetupContract.getRegistry.mockReturnValue(createSetupContractMock()); + return internalSetupContract; +}; + +const createDeprecationsServiceMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockReturnValue(createInternalSetupContractMock()); + return mocked; +}; + +export const deprecationsServiceMock = { + create: createDeprecationsServiceMock, + createInternalSetupContract: createInternalSetupContractMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts new file mode 100644 index 0000000000000..8eca1ba5790c5 --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { DeprecationsFactory } from './deprecations_factory'; +import { RegisterDeprecationsConfig } from './types'; +import { registerRoutes } from './routes'; + +import { CoreContext } from '../core_context'; +import { CoreUsageDataSetup } from '../core_usage_data'; +import { InternalElasticsearchServiceSetup } from '../elasticsearch'; +import { CoreService } from '../../types'; +import { InternalHttpServiceSetup } from '../http'; +import { Logger } from '../logging'; + +/** + * The deprecations service provides a way for the Kibana platform to communicate deprecated + * features and configs with its users. These deprecations are only communicated + * if the deployment is using these features. Allowing for a user tailored experience + * for upgrading the stack version. + * + * The Deprecation service is consumed by the upgrade assistant to assist with the upgrade + * experience. + * + * If a deprecated feature can be resolved without manual user intervention. + * Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the + * deprecation upon a user trigger. + * + * @example + * ```ts + * import { DeprecationsDetails, GetDeprecationsContext, CoreSetup } from 'src/core/server'; + * + * async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { + * const deprecations: DeprecationsDetails[] = []; + * const count = await getTimelionSheetsCount(savedObjectsClient); + * + * if (count > 0) { + * // Example of a manual correctiveAction + * deprecations.push({ + * message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + * documentationUrl: + * 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', + * level: 'warning', + * correctiveActions: { + * manualSteps: [ + * 'Navigate to the Kibana Dashboard and click "Create dashboard".', + * 'Select Timelion from the "New Visualization" window.', + * 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', + * 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', + * 'In the toolbar, click Save.', + * 'On the Save visualization window, enter the visualization Title, then click Save and return.', + * ], + * }, + * }); + * } + * + * // Example of an api correctiveAction + * deprecations.push({ + * "message": "User 'test_dashboard_user' is using a deprecated role: 'kibana_user'", + * "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", + * "level": "critical", + * "correctiveActions": { + * "api": { + * "path": "/internal/security/users/test_dashboard_user", + * "method": "POST", + * "body": { + * "username": "test_dashboard_user", + * "roles": [ + * "machine_learning_user", + * "enrich_user", + * "kibana_admin" + * ], + * "full_name": "Alison Goryachev", + * "email": "alisongoryachev@gmail.com", + * "metadata": {}, + * "enabled": true + * } + * }, + * "manualSteps": [ + * "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", + * "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role." + * ] + * }, + * }); + * + * return deprecations; + * } + * + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.deprecations.registerDeprecations({ getDeprecations }); + * } + * } + * ``` + * + * @public + */ +export interface DeprecationsServiceSetup { + registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; +} + +/** @internal */ +export interface InternalDeprecationsServiceSetup { + getRegistry: (domainId: string) => DeprecationsServiceSetup; +} + +/** @internal */ +export interface DeprecationsSetupDeps { + http: InternalHttpServiceSetup; + elasticsearch: InternalElasticsearchServiceSetup; + coreUsageData: CoreUsageDataSetup; +} + +/** @internal */ +export class DeprecationsService implements CoreService { + private readonly logger: Logger; + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('deprecations-service'); + } + + public setup({ http }: DeprecationsSetupDeps): InternalDeprecationsServiceSetup { + this.logger.debug('Setting up Deprecations service'); + const deprecationsFactory = new DeprecationsFactory({ + logger: this.logger, + }); + + registerRoutes({ http, deprecationsFactory }); + this.registerConfigDeprecationsInfo(deprecationsFactory); + + return { + getRegistry: (domainId: string): DeprecationsServiceSetup => { + const registry = deprecationsFactory.getRegistry(domainId); + return { + registerDeprecations: registry.registerDeprecations, + }; + }, + }; + } + + public start() {} + public stop() {} + + private registerConfigDeprecationsInfo(deprecationsFactory: DeprecationsFactory) { + const handledDeprecatedConfigs = this.coreContext.configService.getHandledDeprecatedConfigs(); + + for (const [domainId, deprecationsContexts] of handledDeprecatedConfigs) { + const deprecationsRegistry = deprecationsFactory.getRegistry(domainId); + deprecationsRegistry.registerDeprecations({ + getDeprecations: () => { + return deprecationsContexts.map(({ message, correctiveActions, documentationUrl }) => { + return { + level: 'critical', + message, + correctiveActions: correctiveActions ?? {}, + documentationUrl, + }; + }); + }, + }); + } + } +} diff --git a/src/core/server/deprecations/index.ts b/src/core/server/deprecations/index.ts new file mode 100644 index 0000000000000..c7d1a13800694 --- /dev/null +++ b/src/core/server/deprecations/index.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 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 type { + DeprecationsDetails, + GetDeprecationsContext, + RegisterDeprecationsConfig, + DeprecationsGetResponse, +} from './types'; + +export type { + DeprecationsServiceSetup, + InternalDeprecationsServiceSetup, +} from './deprecations_service'; + +export { DeprecationsService } from './deprecations_service'; diff --git a/src/core/server/deprecations/routes/get.ts b/src/core/server/deprecations/routes/get.ts new file mode 100644 index 0000000000000..fed3fcfbd1809 --- /dev/null +++ b/src/core/server/deprecations/routes/get.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { IRouter } from '../../http'; +import { GetDeprecationsContext, DeprecationsGetResponse } from '../types'; +import { DeprecationsFactory } from '../deprecations_factory'; + +interface RouteDependencies { + deprecationsFactory: DeprecationsFactory; +} + +export const registerGetRoute = (router: IRouter, { deprecationsFactory }: RouteDependencies) => { + router.get( + { + path: '/', + validate: false, + }, + async (context, req, res) => { + const dependencies: GetDeprecationsContext = { + esClient: context.core.elasticsearch.client, + savedObjectsClient: context.core.savedObjects.client, + }; + + const body: DeprecationsGetResponse = { + deprecations: await deprecationsFactory.getAllDeprecations(dependencies), + }; + + return res.ok({ body }); + } + ); +}; diff --git a/src/core/server/deprecations/routes/index.ts b/src/core/server/deprecations/routes/index.ts new file mode 100644 index 0000000000000..db58bec29f7b8 --- /dev/null +++ b/src/core/server/deprecations/routes/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { InternalHttpServiceSetup } from '../../http'; +import { registerGetRoute } from './get'; +import { DeprecationsFactory } from '../deprecations_factory'; + +export function registerRoutes({ + http, + deprecationsFactory, +}: { + http: InternalHttpServiceSetup; + deprecationsFactory: DeprecationsFactory; +}) { + const router = http.createRouter('/api/deprecations'); + registerGetRoute(router, { deprecationsFactory }); +} diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts new file mode 100644 index 0000000000000..31734b51b46bd --- /dev/null +++ b/src/core/server/deprecations/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright 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 { SavedObjectsClientContract } from '../saved_objects/types'; +import type { IScopedClusterClient } from '../elasticsearch'; + +type MaybePromise = T | Promise; + +export interface DomainDeprecationDetails extends DeprecationsDetails { + domainId: string; +} + +export interface DeprecationsDetails { + /* The message to be displayed for the deprecation. */ + message: string; + /** + * levels: + * - warning: will not break deployment upon upgrade + * - critical: needs to be addressed before upgrade. + * - fetch_error: Deprecations service failed to grab the deprecation details for the domain. + */ + level: 'warning' | 'critical' | 'fetch_error'; + /* (optional) link to the documentation for more details on the deprecation. */ + documentationUrl?: string; + /* corrective action needed to fix this deprecation. */ + correctiveActions: { + /** + * (optional) The api to be called to automatically fix the deprecation + * Each domain should implement a POST/PUT route for their plugin to + * handle their deprecations. + */ + api?: { + /* Kibana route path. Passing a query string is allowed */ + path: string; + /* Kibana route method: 'POST' or 'PUT'. */ + method: 'POST' | 'PUT'; + /* Additional details to be passed to the route. */ + body?: { + [key: string]: any; + }; + }; + /** + * (optional) If this deprecation cannot be automtically fixed + * via an API corrective action. Specify a list of manual steps + * users need to follow to fix the deprecation before upgrade. + */ + manualSteps?: string[]; + }; +} + +export interface RegisterDeprecationsConfig { + getDeprecations: (context: GetDeprecationsContext) => MaybePromise; +} + +export interface GetDeprecationsContext { + esClient: IScopedClusterClient; + savedObjectsClient: SavedObjectsClientContract; +} + +export interface DeprecationsGetResponse { + deprecations: DomainDeprecationDetails[]; +} diff --git a/src/core/server/dev/dev_config.ts b/src/core/server/dev/dev_config.ts index 3a303a61c8563..2fec778d85713 100644 --- a/src/core/server/dev/dev_config.ts +++ b/src/core/server/dev/dev_config.ts @@ -6,26 +6,11 @@ * Side Public License, v 1. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; export const config = { path: 'dev', - schema: schema.object({ - basePathProxyTarget: schema.number({ - defaultValue: 5603, - }), - }), + // dev configuration is validated by the dev cli. + // we only need to register the `dev` schema to avoid failing core's config validation + schema: schema.object({}, { unknowns: 'ignore' }), }; - -export type DevConfigType = TypeOf; - -export class DevConfig { - public basePathProxyTargetPort: number; - - /** - * @internal - */ - constructor(rawConfig: DevConfigType) { - this.basePathProxyTargetPort = rawConfig.basePathProxyTarget; - } -} diff --git a/src/core/server/dev/index.ts b/src/core/server/dev/index.ts index 6e0fd343d2ec8..70257d2a5e6c5 100644 --- a/src/core/server/dev/index.ts +++ b/src/core/server/dev/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { config, DevConfig } from './dev_config'; -export type { DevConfigType } from './dev_config'; +export { config } from './dev_config'; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index 6186df3383235..a7fbce7180223 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -156,9 +156,11 @@ const createErrorTransportRequestPromise = (err: any): MockedTransportRequestPro return promise as MockedTransportRequestPromise; }; -function createApiResponse(opts: Partial = {}): ApiResponse { +function createApiResponse>( + opts: Partial> = {} +): ApiResponse { return { - body: {}, + body: {} as any, statusCode: 200, headers: {}, warnings: [], diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index 7b442469838f6..636841316941b 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -11,7 +11,7 @@ import { elasticsearchClientMock } from './mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; -const dummyBody = { foo: 'bar' }; +const dummyBody: any = { foo: 'bar' }; const createErrorReturn = (err: any) => elasticsearchClientMock.createErrorTransportRequestPromise(err); @@ -29,7 +29,7 @@ describe('retryCallCluster', () => { client.asyncSearch.get.mockReturnValue(successReturn); - const result = await retryCallCluster(() => client.asyncSearch.get()); + const result = await retryCallCluster(() => client.asyncSearch.get({} as any)); expect(result.body).toEqual(dummyBody); }); @@ -44,7 +44,7 @@ describe('retryCallCluster', () => { ) .mockImplementationOnce(() => successReturn); - const result = await retryCallCluster(() => client.asyncSearch.get()); + const result = await retryCallCluster(() => client.asyncSearch.get({} as any)); expect(result.body).toEqual(dummyBody); }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts index 32602849d2e45..63b2233b06a96 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts @@ -11,7 +11,7 @@ jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); export const mockReadPkcs12Keystore = jest.fn(); export const mockReadPkcs12Truststore = jest.fn(); -jest.mock('../utils', () => ({ +jest.mock('@kbn/crypto', () => ({ readPkcs12Keystore: mockReadPkcs12Keystore, readPkcs12Truststore: mockReadPkcs12Truststore, })); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index d3f9693bab229..23b804b535405 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -28,7 +28,7 @@ const applyElasticsearchDeprecations = (settings: Record = {}) => { deprecation, path: CONFIG_PATH, })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, @@ -244,12 +244,12 @@ describe('throws when config is invalid', () => { beforeAll(() => { const realFs = jest.requireActual('fs'); mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); - const utils = jest.requireActual('../utils'); + const crypto = jest.requireActual('@kbn/crypto'); mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => - utils.readPkcs12Keystore(path, password) + crypto.readPkcs12Keystore(path, password) ); mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => - utils.readPkcs12Truststore(path, password) + crypto.readPkcs12Truststore(path, password) ); }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 879002a6ece51..e731af4817955 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -7,10 +7,10 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { readPkcs12Keystore, readPkcs12Truststore } from '@kbn/crypto'; import { Duration } from 'moment'; import { readFileSync } from 'fs'; import { ConfigDeprecationProvider } from 'src/core/server'; -import { readPkcs12Keystore, readPkcs12Truststore } from '../utils'; import { ServiceConfigDescriptor } from '../internal_types'; import { getReservedHeaders } from './default_headers'; @@ -144,32 +144,32 @@ export const configSchema = schema.object({ }); const deprecations: ConfigDeprecationProvider = () => [ - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const es = settings[fromPath]; if (!es) { return settings; } if (es.username === 'elastic') { - log( - `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.` - ); + addDeprecation({ + message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, + }); } else if (es.username === 'kibana') { - log( - `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.` - ); + addDeprecation({ + message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, + }); } if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { - log( - `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` - ); + addDeprecation({ + message: `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + }); } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { - log( - `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` - ); + addDeprecation({ + message: `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + }); } else if (es.logQueries === true) { - log( - `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".` - ); + addDeprecation({ + message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`, + }); } return settings; }, diff --git a/src/core/server/elasticsearch/integration_tests/client.test.ts b/src/core/server/elasticsearch/integration_tests/client.test.ts new file mode 100644 index 0000000000000..3a4b7c5c4af22 --- /dev/null +++ b/src/core/server/elasticsearch/integration_tests/client.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, +} from '../../../test_helpers/kbn_server'; + +describe('elasticsearch clients', () => { + let esServer: TestElasticsearchUtils; + let kibanaServer: TestKibanaUtils; + + beforeAll(async () => { + const { startES, startKibana } = createTestServers({ + adjustTimeout: jest.setTimeout, + }); + + esServer = await startES(); + kibanaServer = await startKibana(); + }); + + afterAll(async () => { + await kibanaServer.stop(); + await esServer.stop(); + }); + + it('does not return deprecation warning when x-elastic-product-origin header is set', async () => { + // Header should be automatically set by Core + const resp1 = await kibanaServer.coreStart.elasticsearch.client.asInternalUser.indices.getSettings( + { index: '.kibana' } + ); + expect(resp1.headers).not.toHaveProperty('warning'); + + // Also test setting it explicitly + const resp2 = await kibanaServer.coreStart.elasticsearch.client.asInternalUser.indices.getSettings( + { index: '.kibana' }, + { headers: { 'x-elastic-product-origin': 'kibana' } } + ); + expect(resp2.headers).not.toHaveProperty('warning'); + }); + + it('returns deprecation warning when x-elastic-product-orign header is not set', async () => { + const resp = await kibanaServer.coreStart.elasticsearch.client.asInternalUser.indices.getSettings( + { index: '.kibana' }, + { headers: { 'x-elastic-product-origin': null } } + ); + + expect(resp.headers).toHaveProperty('warning'); + expect(resp.headers!.warning).toMatch('system indices'); + }); +}); diff --git a/src/core/server/external_url/external_url_config.ts b/src/core/server/external_url/external_url_config.ts index 7e4afbfbfea05..da4e8199dc623 100644 --- a/src/core/server/external_url/external_url_config.ts +++ b/src/core/server/external_url/external_url_config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createSHA256Hash } from '../utils'; +import { createSHA256Hash } from '@kbn/crypto'; import { config } from './config'; const DEFAULT_CONFIG = Object.freeze(config.schema.validate({})); diff --git a/src/core/server/http/base_path_proxy_server.test.ts b/src/core/server/http/base_path_proxy_server.test.ts deleted file mode 100644 index 80c03a2af9031..0000000000000 --- a/src/core/server/http/base_path_proxy_server.test.ts +++ /dev/null @@ -1,1021 +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 { BasePathProxyServer, BasePathProxyServerOptions } from './base_path_proxy_server'; -import { loggingSystemMock } from '../logging/logging_system.mock'; -import { DevConfig } from '../dev/dev_config'; -import { EMPTY } from 'rxjs'; -import { HttpConfig } from './http_config'; -import { ByteSizeValue, schema } from '@kbn/config-schema'; -import { - KibanaRequest, - KibanaResponseFactory, - Router, - RouteValidationFunction, - RouteValidationResultFactory, -} from './router'; -import { HttpServer } from './http_server'; -import supertest from 'supertest'; -import { RequestHandlerContext } from 'kibana/server'; -import { readFileSync } from 'fs'; -import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; -import { omit } from 'lodash'; -import { Readable } from 'stream'; - -/** - * Most of these tests are inspired by: - * src/core/server/http/http_server.test.ts - * and copied for completeness from that file. The modifications are that these tests use the developer proxy. - */ -describe('BasePathProxyServer', () => { - let server: HttpServer; - let proxyServer: BasePathProxyServer; - let config: HttpConfig; - let configWithSSL: HttpConfig; - let basePath: string; - let certificate: string; - let key: string; - let proxySupertest: supertest.SuperTest; - const logger = loggingSystemMock.createLogger(); - const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); - - beforeAll(() => { - certificate = readFileSync(KBN_CERT_PATH, 'utf8'); - key = readFileSync(KBN_KEY_PATH, 'utf8'); - }); - - beforeEach(async () => { - // setup the server but don't start it until each individual test so that routes can be dynamically configured per unit test. - server = new HttpServer(logger, 'tests'); - config = ({ - name: 'kibana', - host: '127.0.0.1', - port: 10012, - compression: { enabled: true }, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - autoListen: true, - keepaliveTimeout: 1000, - socketTimeout: 1000, - cors: { - enabled: false, - allowCredentials: false, - allowOrigin: [], - }, - ssl: { enabled: false }, - customResponseHeaders: {}, - maxPayload: new ByteSizeValue(1024), - rewriteBasePath: true, - } as unknown) as HttpConfig; - - configWithSSL = { - ...config, - ssl: { - enabled: true, - certificate, - cipherSuites: ['TLS_AES_256_GCM_SHA384'], - getSecureOptions: () => 0, - key, - redirectHttpFromPort: config.port + 1, - }, - } as HttpConfig; - - // setup and start the proxy server - const proxyConfig: HttpConfig = { ...config, port: 10013 }; - const devConfig = new DevConfig({ basePathProxyTarget: config.port }); - proxyServer = new BasePathProxyServer(logger, proxyConfig, devConfig); - const options: Readonly = { - shouldRedirectFromOldBasePath: () => true, - delayUntil: () => EMPTY, - }; - await proxyServer.start(options); - - // set the base path or throw if for some unknown reason it is not setup - if (proxyServer.basePath == null) { - throw new Error('Invalid null base path, all tests will fail'); - } else { - basePath = proxyServer.basePath; - } - proxySupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); - }); - - afterEach(async () => { - await server.stop(); - await proxyServer.stop(); - jest.clearAllMocks(); - }); - - test('root URL will return a 302 redirect', async () => { - await proxySupertest.get('/').expect(302); - }); - - test('root URL will return a redirect location with exactly 3 characters that are a-z', async () => { - const res = await proxySupertest.get('/'); - const location = res.header.location; - expect(location).toMatch(/[a-z]{3}/); - }); - - test('valid params', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - router.get( - { - path: '/{test}', - validate: { - params: schema.object({ - test: schema.string(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: req.params.test }); - } - ); - const { registerRouter } = await server.setup(config); - registerRouter(router); - await server.start(); - - await proxySupertest - .get(`${basePath}/foo/some-string`) - .expect(200) - .then((res) => { - expect(res.text).toBe('some-string'); - }); - }); - - test('invalid params', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.get( - { - path: '/{test}', - validate: { - params: schema.object({ - test: schema.number(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: String(req.params.test) }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .get(`${basePath}/foo/some-string`) - .expect(400) - .then((res) => { - expect(res.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: '[request params.test]: expected value of type [number] but got [string]', - }); - }); - }); - - test('valid query', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.get( - { - path: '/', - validate: { - query: schema.object({ - bar: schema.string(), - quux: schema.number(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: req.query }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .get(`${basePath}/foo/?bar=test&quux=123`) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ bar: 'test', quux: 123 }); - }); - }); - - test('invalid query', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.get( - { - path: '/', - validate: { - query: schema.object({ - bar: schema.number(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: req.query }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .get(`${basePath}/foo/?bar=test`) - .expect(400) - .then((res) => { - expect(res.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: '[request query.bar]: expected value of type [number] but got [string]', - }); - }); - }); - - test('valid body', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.post( - { - path: '/', - validate: { - body: schema.object({ - bar: schema.string(), - baz: schema.number(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: req.body }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .post(`${basePath}/foo/`) - .send({ - bar: 'test', - baz: 123, - }) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ bar: 'test', baz: 123 }); - }); - }); - - test('valid body with validate function', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.post( - { - path: '/', - validate: { - body: ({ bar, baz } = {}, { ok, badRequest }) => { - if (typeof bar === 'string' && typeof baz === 'number') { - return ok({ bar, baz }); - } else { - return badRequest('Wrong payload', ['body']); - } - }, - }, - }, - (_, req, res) => { - return res.ok({ body: req.body }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .post(`${basePath}/foo/`) - .send({ - bar: 'test', - baz: 123, - }) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ bar: 'test', baz: 123 }); - }); - }); - - test('not inline validation - specifying params', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - const bodyValidation = ( - { bar, baz }: any = {}, - { ok, badRequest }: RouteValidationResultFactory - ) => { - if (typeof bar === 'string' && typeof baz === 'number') { - return ok({ bar, baz }); - } else { - return badRequest('Wrong payload', ['body']); - } - }; - - router.post( - { - path: '/', - validate: { - body: bodyValidation, - }, - }, - (_, req, res) => { - return res.ok({ body: req.body }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .post(`${basePath}/foo/`) - .send({ - bar: 'test', - baz: 123, - }) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ bar: 'test', baz: 123 }); - }); - }); - - test('not inline validation - specifying validation handler', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - const bodyValidation: RouteValidationFunction<{ bar: string; baz: number }> = ( - { bar, baz } = {}, - { ok, badRequest } - ) => { - if (typeof bar === 'string' && typeof baz === 'number') { - return ok({ bar, baz }); - } else { - return badRequest('Wrong payload', ['body']); - } - }; - - router.post( - { - path: '/', - validate: { - body: bodyValidation, - }, - }, - (_, req, res) => { - return res.ok({ body: req.body }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .post(`${basePath}/foo/`) - .send({ - bar: 'test', - baz: 123, - }) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ bar: 'test', baz: 123 }); - }); - }); - - test('not inline handler - KibanaRequest', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - const handler = ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ) => { - const body = { - bar: req.body.bar.toUpperCase(), - baz: req.body.baz.toString(), - }; - - return res.ok({ body }); - }; - - router.post( - { - path: '/', - validate: { - body: ({ bar, baz } = {}, { ok, badRequest }) => { - if (typeof bar === 'string' && typeof baz === 'number') { - return ok({ bar, baz }); - } else { - return badRequest('Wrong payload', ['body']); - } - }, - }, - }, - handler - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .post(`${basePath}/foo/`) - .send({ - bar: 'test', - baz: 123, - }) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ bar: 'TEST', baz: '123' }); - }); - }); - - test('invalid body', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.post( - { - path: '/', - validate: { - body: schema.object({ - bar: schema.number(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: req.body }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .post(`${basePath}/foo/`) - .send({ bar: 'test' }) - .expect(400) - .then((res) => { - expect(res.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: '[request body.bar]: expected value of type [number] but got [string]', - }); - }); - }); - - test('handles putting', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.put( - { - path: '/', - validate: { - body: schema.object({ - key: schema.string(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: req.body }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .put(`${basePath}/foo/`) - .send({ key: 'new value' }) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ key: 'new value' }); - }); - }); - - test('handles deleting', async () => { - const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); - - router.delete( - { - path: '/{id}', - validate: { - params: schema.object({ - id: schema.number(), - }), - }, - }, - (_, req, res) => { - return res.ok({ body: { key: req.params.id } }); - } - ); - - const { registerRouter } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await proxySupertest - .delete(`${basePath}/foo/3`) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ key: 3 }); - }); - }); - - describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { - let configWithBasePath: HttpConfig; - - beforeEach(async () => { - configWithBasePath = { - ...config, - basePath: '/bar', - rewriteBasePath: false, - } as HttpConfig; - - const router = new Router(`${basePath}/`, logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (_, __, res) => res.ok({ body: 'value:/' })); - router.get({ path: '/foo', validate: false }, (_, __, res) => res.ok({ body: 'value:/foo' })); - - const { registerRouter } = await server.setup(configWithBasePath); - registerRouter(router); - - await server.start(); - }); - - test('/bar => 404', async () => { - await proxySupertest.get(`${basePath}/bar`).expect(404); - }); - - test('/bar/ => 404', async () => { - await proxySupertest.get(`${basePath}/bar/`).expect(404); - }); - - test('/bar/foo => 404', async () => { - await proxySupertest.get(`${basePath}/bar/foo`).expect(404); - }); - - test('/ => /', async () => { - await proxySupertest - .get(`${basePath}/`) - .expect(200) - .then((res) => { - expect(res.text).toBe('value:/'); - }); - }); - - test('/foo => /foo', async () => { - await proxySupertest - .get(`${basePath}/foo`) - .expect(200) - .then((res) => { - expect(res.text).toBe('value:/foo'); - }); - }); - }); - - test('with defined `redirectHttpFromPort`', async () => { - const router = new Router(`${basePath}/`, logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (_, __, res) => res.ok({ body: 'value:/' })); - - const { registerRouter } = await server.setup(configWithSSL); - registerRouter(router); - - await server.start(); - }); - - test('allows attaching metadata to attach meta-data tag strings to a route', async () => { - const tags = ['my:tag']; - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.get({ path: '/with-tags', validate: false, options: { tags } }, (_, req, res) => - res.ok({ body: { tags: req.route.options.tags } }) - ); - router.get({ path: '/without-tags', validate: false }, (_, req, res) => - res.ok({ body: { tags: req.route.options.tags } }) - ); - registerRouter(router); - - await server.start(); - await proxySupertest.get(`${basePath}/with-tags`).expect(200, { tags }); - - await proxySupertest.get(`${basePath}/without-tags`).expect(200, { tags: [] }); - }); - - describe('response headers', () => { - test('default headers', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (_, req, res) => res.ok({ body: req.route })); - registerRouter(router); - - await server.start(); - const response = await proxySupertest.get(`${basePath}/`).expect(200); - - const restHeaders = omit(response.header, ['date', 'content-length']); - expect(restHeaders).toMatchInlineSnapshot(` - Object { - "accept-ranges": "bytes", - "cache-control": "private, no-cache, no-store, must-revalidate", - "connection": "close", - "content-type": "application/json; charset=utf-8", - } - `); - }); - }); - - test('exposes route details of incoming request to a route handler (POST + payload options)', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.post( - { - path: '/', - validate: { body: schema.object({ test: schema.number() }) }, - options: { body: { accepts: 'application/json' } }, - }, - (_, req, res) => res.ok({ body: req.route }) - ); - registerRouter(router); - - await server.start(); - await proxySupertest - .post(`${basePath}/`) - .send({ test: 1 }) - .expect(200, { - method: 'post', - path: `${basePath}/`, - options: { - authRequired: true, - xsrfRequired: true, - tags: [], - timeout: { - payload: 10000, - idleSocket: 1000, - }, - body: { - parse: true, // hapi populates the default - maxBytes: 1024, // hapi populates the default - accepts: ['application/json'], - output: 'data', - }, - }, - }); - }); - - test('should return a stream in the body', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.put( - { - path: '/', - validate: { body: schema.stream() }, - options: { body: { output: 'stream' } }, - }, - (_, req, res) => { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } - ); - registerRouter(router); - - await server.start(); - await proxySupertest.put(`${basePath}/`).send({ test: 1 }).expect(200, { - parse: true, - maxBytes: 1024, // hapi populates the default - output: 'stream', - }); - }); - - describe('timeout options', () => { - describe('payload timeout', () => { - test('POST routes set the payload timeout', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.post( - { - path: '/', - validate: false, - options: { - timeout: { - payload: 300000, - }, - }, - }, - (_, req, res) => { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } - ); - registerRouter(router); - await server.start(); - await proxySupertest - .post(`${basePath}/`) - .send({ test: 1 }) - .expect(200, { - timeout: { - payload: 300000, - idleSocket: 1000, // This is an extra option added by the proxy - }, - }); - }); - - test('DELETE routes set the payload timeout', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.delete( - { - path: '/', - validate: false, - options: { - timeout: { - payload: 300000, - }, - }, - }, - (context, req, res) => { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } - ); - registerRouter(router); - await server.start(); - await proxySupertest.delete(`${basePath}/`).expect(200, { - timeout: { - payload: 300000, - idleSocket: 1000, // This is an extra option added by the proxy - }, - }); - }); - - test('PUT routes set the payload timeout and automatically adjusts the idle socket timeout', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.put( - { - path: '/', - validate: false, - options: { - timeout: { - payload: 300000, - }, - }, - }, - (_, req, res) => { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } - ); - registerRouter(router); - await server.start(); - await proxySupertest.put(`${basePath}/`).expect(200, { - timeout: { - payload: 300000, - idleSocket: 1000, // This is an extra option added by the proxy - }, - }); - }); - - test('PATCH routes set the payload timeout and automatically adjusts the idle socket timeout', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.patch( - { - path: '/', - validate: false, - options: { - timeout: { - payload: 300000, - }, - }, - }, - (_, req, res) => { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } - ); - registerRouter(router); - await server.start(); - await proxySupertest.patch(`${basePath}/`).expect(200, { - timeout: { - payload: 300000, - idleSocket: 1000, // This is an extra option added by the proxy - }, - }); - }); - }); - - describe('idleSocket timeout', () => { - test('uses server socket timeout when not specified in the route', async () => { - const { registerRouter } = await server.setup({ - ...config, - socketTimeout: 11000, - }); - - const router = new Router(basePath, logger, enhanceWithContext); - router.get( - { - path: '/', - validate: { body: schema.maybe(schema.any()) }, - }, - (_, req, res) => { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } - ); - registerRouter(router); - - await server.start(); - await proxySupertest - .get(`${basePath}/`) - .send() - .expect(200, { - timeout: { - idleSocket: 11000, - }, - }); - }); - - test('sets the socket timeout when specified in the route', async () => { - const { registerRouter } = await server.setup({ - ...config, - socketTimeout: 11000, - }); - - const router = new Router(basePath, logger, enhanceWithContext); - router.get( - { - path: '/', - validate: { body: schema.maybe(schema.any()) }, - options: { timeout: { idleSocket: 12000 } }, - }, - (context, req, res) => { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } - ); - registerRouter(router); - - await server.start(); - await proxySupertest - .get(`${basePath}/`) - .send() - .expect(200, { - timeout: { - idleSocket: 12000, - }, - }); - }); - - test('idleSocket timeout can be smaller than the payload timeout', async () => { - const { registerRouter } = await server.setup(config); - - const router = new Router(basePath, logger, enhanceWithContext); - router.post( - { - path: `${basePath}/`, - validate: { body: schema.any() }, - options: { - timeout: { - payload: 1000, - idleSocket: 10, - }, - }, - }, - (_, req, res) => { - return res.ok({ body: { timeout: req.route.options.timeout } }); - } - ); - - registerRouter(router); - - await server.start(); - }); - }); - }); - - describe('shouldRedirect', () => { - let proxyServerWithoutShouldRedirect: BasePathProxyServer; - let proxyWithoutShouldRedirectSupertest: supertest.SuperTest; - - beforeEach(async () => { - // setup and start a proxy server which does not use "shouldRedirectFromOldBasePath" - const proxyConfig: HttpConfig = { ...config, port: 10004 }; - const devConfig = new DevConfig({ basePathProxyTarget: config.port }); - proxyServerWithoutShouldRedirect = new BasePathProxyServer(logger, proxyConfig, devConfig); - const options: Readonly = { - shouldRedirectFromOldBasePath: () => false, // Return false to not redirect - delayUntil: () => EMPTY, - }; - await proxyServerWithoutShouldRedirect.start(options); - proxyWithoutShouldRedirectSupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); - }); - - afterEach(async () => { - await proxyServerWithoutShouldRedirect.stop(); - }); - - test('it will do a redirect if it detects what looks like a stale or previously used base path', async () => { - const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; - const res = await proxySupertest.get(`/${fakeBasePath}`).expect(302); - const location = res.header.location; - expect(location).toEqual(`${basePath}/`); - }); - - test('it will NOT do a redirect if it detects what looks like a stale or previously used base path if we intentionally turn it off', async () => { - const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; - await proxyWithoutShouldRedirectSupertest.get(`/${fakeBasePath}`).expect(404); - }); - - test('it will NOT redirect if it detects a larger path than 3 characters', async () => { - await proxySupertest.get('/abcde').expect(404); - }); - - test('it will NOT redirect if it is not a GET verb', async () => { - const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; - await proxySupertest.put(`/${fakeBasePath}`).expect(404); - }); - }); - - describe('constructor option for sending in a custom basePath', () => { - let proxyServerWithFooBasePath: BasePathProxyServer; - let proxyWithFooBasePath: supertest.SuperTest; - - beforeEach(async () => { - // setup and start a proxy server which uses a basePath of "foo" - const proxyConfig: HttpConfig = { ...config, port: 10004, basePath: '/foo' }; // <-- "foo" here in basePath - const devConfig = new DevConfig({ basePathProxyTarget: config.port }); - proxyServerWithFooBasePath = new BasePathProxyServer(logger, proxyConfig, devConfig); - const options: Readonly = { - shouldRedirectFromOldBasePath: () => true, - delayUntil: () => EMPTY, - }; - await proxyServerWithFooBasePath.start(options); - proxyWithFooBasePath = supertest(`http://127.0.0.1:${proxyConfig.port}`); - }); - - afterEach(async () => { - await proxyServerWithFooBasePath.stop(); - }); - - test('it will do a redirect to foo which is our passed in value for the configuration', async () => { - const res = await proxyWithFooBasePath.get('/bar').expect(302); - const location = res.header.location; - expect(location).toEqual('/foo/'); - }); - }); -}); diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index f00cbb928d631..c802163866423 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -124,7 +124,9 @@ const cookieOptions = { path, }; -describe('Cookie based SessionStorage', () => { +// FLAKY: https://github.com/elastic/kibana/issues/89318 +// https://github.com/elastic/kibana/issues/89319 +describe.skip('Cookie based SessionStorage', () => { describe('#set()', () => { it('Should write to session storage & set cookies', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 2bbe9f3f96a55..356dad201ce95 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -7,12 +7,12 @@ */ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; +import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; import { hostname } from 'os'; import url from 'url'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url'; -import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /^\/.*[^\/]$/; const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i; @@ -156,7 +156,7 @@ export const config = { }; export type HttpConfigType = TypeOf; -export class HttpConfig { +export class HttpConfig implements IHttpConfig { public name: string; public autoListen: boolean; public host: string; diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 54be7b35f68ad..ccd14d4b99e11 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -1288,6 +1288,30 @@ test('should return a stream in the body', async () => { }); }); +test('closes sockets on timeout', async () => { + const { registerRouter, server: innerServer } = await server.setup({ + ...config, + socketTimeout: 1000, + }); + const router = new Router('', logger, enhanceWithContext); + + router.get({ path: '/a', validate: false }, async (context, req, res) => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + return res.ok({}); + }); + router.get({ path: '/b', validate: false }, (context, req, res) => res.ok({})); + + registerRouter(router); + + registerRouter(router); + + await server.start(); + + expect(supertest(innerServer.listener).get('/a')).rejects.toThrow('socket hang up'); + + await supertest(innerServer.listener).get('/b').expect(200); +}); + describe('setup contract', () => { describe('#createSessionStorage', () => { test('creates session storage factory', async () => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index b0510bc414bf8..cd7d7ccc5aeff 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -10,10 +10,15 @@ import { Server, Request } from '@hapi/hapi'; import HapiStaticFiles from '@hapi/inert'; import url from 'url'; import uuid from 'uuid'; +import { + createServer, + getListenerOptions, + getServerOptions, + getRequestId, +} from '@kbn/server-http-tools'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; -import { createServer, getListenerOptions, getServerOptions, getRequestId } from './http_tools'; import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 9354c89b63292..83279e99bc476 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -242,29 +242,6 @@ test('returns http server contract on setup', async () => { }); }); -test('does not start http server if process is dev cluster master', async () => { - const configService = createConfigService(); - const httpServer = { - isListening: () => false, - setup: jest.fn().mockReturnValue({}), - start: jest.fn(), - stop: noop, - }; - mockHttpServer.mockImplementation(() => httpServer); - - const service = new HttpService({ - coreId, - configService, - env: Env.createDefault(REPO_ROOT, getEnvOptions({ isDevCliParent: true })), - logger, - }); - - await service.setup(setupDeps); - await service.start(); - - expect(httpServer.start).not.toHaveBeenCalled(); -}); - test('does not start http server if configured with `autoListen:false`', async () => { const configService = createConfigService({ autoListen: false, diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 87143e1160c6c..5b90440f6ad70 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -153,15 +153,13 @@ export class HttpService } /** - * Indicates if http server has configured to start listening on a configured port. - * We shouldn't start http service in two cases: - * 1. If `server.autoListen` is explicitly set to `false`. - * 2. When the process is run as dev cluster master in which case cluster manager - * will fork a dedicated process where http service will be set up instead. + * Indicates if http server is configured to start listening on a configured port. + * (if `server.autoListen` is not explicitly set to `false`.) + * * @internal * */ private shouldListen(config: HttpConfig) { - return !this.coreContext.env.isDevCliParent && config.autoListen; + return config.autoListen; } public async stop() { diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts deleted file mode 100644 index c2fa3816324fc..0000000000000 --- a/src/core/server/http/http_tools.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -jest.mock('fs', () => { - const original = jest.requireActual('fs'); - return { - // Hapi Inert patches native methods - ...original, - readFileSync: jest.fn(), - }; -}); - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), -})); - -import supertest from 'supertest'; -import { Request, ResponseToolkit } from '@hapi/hapi'; -import Joi from 'joi'; - -import { - defaultValidationErrorHandler, - HapiValidationError, - getServerOptions, - getRequestId, -} from './http_tools'; -import { HttpServer } from './http_server'; -import { HttpConfig, config } from './http_config'; -import { Router } from './router'; -import { loggingSystemMock } from '../logging/logging_system.mock'; -import { ByteSizeValue } from '@kbn/config-schema'; - -const emptyOutput = { - statusCode: 400, - headers: {}, - payload: { - statusCode: 400, - error: '', - validation: { - source: '', - keys: [], - }, - }, -}; - -afterEach(() => jest.clearAllMocks()); - -describe('defaultValidationErrorHandler', () => { - it('formats value validation errors correctly', () => { - expect.assertions(1); - const schema = Joi.array().items( - Joi.object({ - type: Joi.string().required(), - }).required() - ); - - const error = schema.validate([{}], { abortEarly: false }).error as HapiValidationError; - - // Emulate what Hapi v17 does by default - error.output = { ...emptyOutput }; - error.output.payload.validation.keys = ['0.type', '']; - - try { - defaultValidationErrorHandler({} as Request, {} as ResponseToolkit, error); - } catch (err) { - // Verify the empty string gets corrected to 'value' - expect(err.output.payload.validation.keys).toEqual(['0.type', 'value']); - } - }); -}); - -describe('timeouts', () => { - const logger = loggingSystemMock.create(); - const server = new HttpServer(logger, 'foo'); - const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); - - test('closes sockets on timeout', async () => { - const router = new Router('', logger.get(), enhanceWithContext); - router.get({ path: '/a', validate: false }, async (context, req, res) => { - await new Promise((resolve) => setTimeout(resolve, 2000)); - return res.ok({}); - }); - router.get({ path: '/b', validate: false }, (context, req, res) => res.ok({})); - const { registerRouter, server: innerServer } = await server.setup({ - socketTimeout: 1000, - host: '127.0.0.1', - maxPayload: new ByteSizeValue(1024), - ssl: {}, - cors: { - enabled: false, - }, - compression: { enabled: true }, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - } as any); - registerRouter(router); - - await server.start(); - - expect(supertest(innerServer.listener).get('/a')).rejects.toThrow('socket hang up'); - - await supertest(innerServer.listener).get('/b').expect(200); - }); - - afterAll(async () => { - await server.stop(); - }); -}); - -describe('getServerOptions', () => { - beforeEach(() => - jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`) - ); - - it('properly configures TLS with default options', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - }, - }), - {} as any, - {} as any - ); - - expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` - Object { - "ca": undefined, - "cert": "content-some-certificate-path", - "ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", - "honorCipherOrder": true, - "key": "content-some-key-path", - "passphrase": undefined, - "rejectUnauthorized": false, - "requestCert": false, - "secureOptions": 67108864, - } - `); - }); - - it('properly configures TLS with client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - certificateAuthorities: ['ca-1', 'ca-2'], - clientAuthentication: 'required', - }, - }), - {} as any, - {} as any - ); - - expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` - Object { - "ca": Array [ - "content-ca-1", - "content-ca-2", - ], - "cert": "content-some-certificate-path", - "ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", - "honorCipherOrder": true, - "key": "content-some-key-path", - "passphrase": undefined, - "rejectUnauthorized": true, - "requestCert": true, - "secureOptions": 67108864, - } - `); - }); - - it('properly configures CORS when cors enabled', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - cors: { - enabled: true, - allowCredentials: false, - allowOrigin: ['*'], - }, - }), - {} as any, - {} as any - ); - - expect(getServerOptions(httpConfig).routes?.cors).toEqual({ - credentials: false, - origin: ['*'], - headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'], - }); - }); -}); - -describe('getRequestId', () => { - describe('when allowFromAnyIp is true', () => { - it('generates a UUID if no x-opaque-id header is present', () => { - const request = { - headers: {}, - raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, - } as any; - expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( - 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' - ); - }); - - it('uses x-opaque-id header value if present', () => { - const request = { - headers: { - 'x-opaque-id': 'id from header', - raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, - }, - } as any; - expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( - 'id from header' - ); - }); - }); - - describe('when allowFromAnyIp is false', () => { - describe('and ipAllowlist is empty', () => { - it('generates a UUID even if x-opaque-id header is present', () => { - const request = { - headers: { 'x-opaque-id': 'id from header' }, - raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, - } as any; - expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual( - 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' - ); - }); - }); - - describe('and ipAllowlist is not empty', () => { - it('uses x-opaque-id header if request comes from trusted IP address', () => { - const request = { - headers: { 'x-opaque-id': 'id from header' }, - raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, - } as any; - expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( - 'id from header' - ); - }); - - it('generates a UUID if request comes from untrusted IP address', () => { - const request = { - headers: { 'x-opaque-id': 'id from header' }, - raw: { req: { socket: { remoteAddress: '5.5.5.5' } } }, - } as any; - expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( - 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' - ); - }); - - it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => { - const request = { - headers: {}, - raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, - } as any; - expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( - 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' - ); - }); - }); - }); -}); diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts deleted file mode 100644 index e909b454feae2..0000000000000 --- a/src/core/server/http/http_tools.ts +++ /dev/null @@ -1,186 +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 type { - Lifecycle, - Request, - ResponseToolkit, - RouteOptionsCors, - ServerOptions, - Util, -} from '@hapi/hapi'; -import Hoek from '@hapi/hoek'; -import type { ServerOptions as TLSOptions } from 'https'; -import type { ValidationError } from 'joi'; -import uuid from 'uuid'; -import { ensureNoUnsafeProperties } from '@kbn/std'; -import { HttpConfig } from './http_config'; - -const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf']; -/** - * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. - */ -export function getServerOptions(config: HttpConfig, { configureTLS = true } = {}) { - const cors: RouteOptionsCors | false = config.cors.enabled - ? { - credentials: config.cors.allowCredentials, - origin: config.cors.allowOrigin, - headers: corsAllowedHeaders, - } - : false; - // Note that all connection options configured here should be exactly the same - // as in the legacy platform server (see `src/legacy/server/http/index`). Any change - // SHOULD BE applied in both places. The only exception is TLS-specific options, - // that are configured only here. - const options: ServerOptions = { - host: config.host, - port: config.port, - routes: { - cache: { - privacy: 'private', - otherwise: 'private, no-cache, no-store, must-revalidate', - }, - cors, - payload: { - maxBytes: config.maxPayload.getValueInBytes(), - }, - validate: { - failAction: defaultValidationErrorHandler, - options: { - abortEarly: false, - }, - // TODO: This payload validation can be removed once the legacy platform is completely removed. - // This is a default payload validation which applies to all LP routes which do not specify their own - // `validate.payload` handler, in order to reduce the likelyhood of prototype pollution vulnerabilities. - // (All NP routes are already required to specify their own validation in order to access the payload) - payload: (value) => Promise.resolve(ensureNoUnsafeProperties(value)), - }, - }, - state: { - strictHeader: false, - isHttpOnly: true, - isSameSite: false, // necessary to allow using Kibana inside an iframe - }, - }; - - if (configureTLS && config.ssl.enabled) { - const ssl = config.ssl; - - // TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of - // `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`. - const tlsOptions: TLSOptions = { - ca: ssl.certificateAuthorities, - cert: ssl.certificate, - ciphers: config.ssl.cipherSuites.join(':'), - // We use the server's cipher order rather than the client's to prevent the BEAST attack. - honorCipherOrder: true, - key: ssl.key, - passphrase: ssl.keyPassphrase, - secureOptions: ssl.getSecureOptions(), - requestCert: ssl.requestCert, - rejectUnauthorized: ssl.rejectUnauthorized, - }; - - options.tls = tlsOptions; - } - - return options; -} - -export function getListenerOptions(config: HttpConfig) { - return { - keepaliveTimeout: config.keepaliveTimeout, - socketTimeout: config.socketTimeout, - }; -} - -interface ListenerOptions { - keepaliveTimeout: number; - socketTimeout: number; -} - -export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { - const server = new Server(serverOptions); - - server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; - server.listener.setTimeout(listenerOptions.socketTimeout); - server.listener.on('timeout', (socket) => { - socket.destroy(); - }); - server.listener.on('clientError', (err, socket) => { - if (socket.writable) { - socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')); - } else { - socket.destroy(err); - } - }); - - return server; -} - -/** - * Hapi extends the ValidationError interface to add this output key with more data. - */ -export interface HapiValidationError extends ValidationError { - output: { - statusCode: number; - headers: Util.Dictionary; - payload: { - statusCode: number; - error: string; - message?: string; - validation: { - source: string; - keys: string[]; - }; - }; - }; -} - -/** - * Used to replicate Hapi v16 and below's validation responses. Should be used in the routes.validate.failAction key. - */ -export function defaultValidationErrorHandler( - request: Request, - h: ResponseToolkit, - err?: Error -): Lifecycle.ReturnValue { - // Newer versions of Joi don't format the key for missing params the same way. This shim - // provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class - // in JS so we have to rely on the `name` key before we can cast it. - // - // The Hapi code we're 'overwriting' can be found here: - // https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102 - if (err && err.name === 'ValidationError' && err.hasOwnProperty('output')) { - const validationError: HapiValidationError = err as HapiValidationError; - const validationKeys: string[] = []; - - validationError.details.forEach((detail) => { - if (detail.path.length > 0) { - validationKeys.push(Hoek.escapeHtml(detail.path.join('.'))); - } else { - // If no path, use the value sigil to signal the entire value had an issue. - validationKeys.push('value'); - } - }); - - validationError.output.payload.validation.keys = validationKeys; - } - - throw err; -} - -export function getRequestId(request: Request, options: HttpConfig['requestId']): string { - return options.allowFromAnyIp || - // socket may be undefined in integration tests that connect via the http listener directly - (request.raw.req.socket?.remoteAddress && - options.ipAllowlist.includes(request.raw.req.socket.remoteAddress)) - ? request.headers['x-opaque-id'] ?? uuid.v4() - : uuid.v4(); -} diff --git a/src/core/server/http/https_redirect_server.ts b/src/core/server/http/https_redirect_server.ts index dd29a46d728e7..28909c0308c22 100644 --- a/src/core/server/http/https_redirect_server.ts +++ b/src/core/server/http/https_redirect_server.ts @@ -8,10 +8,10 @@ import { Request, ResponseToolkit, Server } from '@hapi/hapi'; import { format as formatUrl } from 'url'; +import { createServer, getListenerOptions, getServerOptions } from '@kbn/server-http-tools'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; -import { createServer, getListenerOptions, getServerOptions } from './http_tools'; export class HttpsRedirectServer { private server?: Server; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index c35b7e2fcd042..84fe5149c89c6 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -56,7 +56,6 @@ export type { DestructiveRouteMethod, SafeRouteMethod, } from './router'; -export { BasePathProxyServer } from './base_path_proxy_server'; export type { OnPreRoutingHandler, OnPreRoutingToolkit } from './lifecycle/on_pre_routing'; export type { AuthenticationHandler, 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 6c11534df0d11..af358caae8bfc 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -540,5 +540,50 @@ describe('http service', () => { expect(header['www-authenticate']).toEqual('Basic realm="Authorization Required"'); }); + + it('provides error reason for Elasticsearch Response Errors', async () => { + const { http } = await root.setup(); + const { createRouter } = http; + // eslint-disable-next-line prefer-const + let elasticsearch: InternalElasticsearchServiceStart; + + esClient.ping.mockImplementation(() => + elasticsearchClientMock.createErrorTransportRequestPromise( + new ResponseError({ + statusCode: 404, + body: { + error: { + type: 'error_type', + reason: 'error_reason', + }, + }, + warnings: [], + headers: {}, + meta: {} as any, + }) + ) + ); + + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + try { + const result = await elasticsearch.client.asScoped(req).asInternalUser.ping(); + return res.ok({ + body: result, + }); + } catch (e) { + return res.badRequest({ + body: e, + }); + } + }); + + const coreStart = await root.start(); + elasticsearch = coreStart.elasticsearch; + + const { body } = await kbnTestServer.request.get(root, '/new-platform/').expect(400); + + expect(body.message).toEqual('[error_type]: error_reason'); + }); }); }); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 03324dc6c722f..5b297ab44f8bb 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -11,14 +11,12 @@ import Boom from '@hapi/boom'; import supertest from 'supertest'; import { schema } from '@kbn/config-schema'; -import { HttpService } from '../http_service'; - import { contextServiceMock } from '../../context/context_service.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; +import { HttpService } from '../http_service'; let server: HttpService; - let logger: ReturnType; const contextSetup = contextServiceMock.createSetupContract(); @@ -28,7 +26,6 @@ const setupDeps = { beforeEach(() => { logger = loggingSystemMock.create(); - server = createHttpServer({ logger }); }); diff --git a/src/core/server/http/router/response_adapter.ts b/src/core/server/http/router/response_adapter.ts index 15c29e261c30b..32a66adc697cf 100644 --- a/src/core/server/http/router/response_adapter.ts +++ b/src/core/server/http/router/response_adapter.ts @@ -14,6 +14,8 @@ import typeDetect from 'type-detect'; import Boom from '@hapi/boom'; import * as stream from 'stream'; +import { isResponseError as isElasticsearchResponseError } from '../../elasticsearch/client/errors'; + import { HttpResponsePayload, KibanaResponse, @@ -147,6 +149,11 @@ function getErrorMessage(payload?: ResponseError): string { throw new Error('expected error message to be provided'); } if (typeof payload === 'string') return payload; + // for ES response errors include nested error reason message. it doesn't contain sensitive data. + if (isElasticsearchResponseError(payload)) { + return `[${payload.message}]: ${payload.meta.body?.error?.reason}`; + } + return getErrorMessage(payload.message); } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 3e336dceb83d7..963b69eac4f7f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -57,7 +57,7 @@ import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; - +import { DeprecationsServiceSetup } from './deprecations'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected import { @@ -88,8 +88,8 @@ export type { ConfigService, ConfigDeprecation, ConfigDeprecationProvider, - ConfigDeprecationLogger, ConfigDeprecationFactory, + AddConfigDeprecation, EnvironmentMode, PackageInfo, } from './config'; @@ -282,6 +282,9 @@ export type { SavedObjectsClientFactoryProvider, SavedObjectsClosePointInTimeOptions, SavedObjectsClosePointInTimeResponse, + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreateOptions, SavedObjectsExportResultDetails, SavedObjectsFindResult, @@ -378,6 +381,12 @@ export type { } from './metrics'; export type { I18nServiceSetup } from './i18n'; +export type { + DeprecationsDetails, + RegisterDeprecationsConfig, + GetDeprecationsContext, + DeprecationsServiceSetup, +} from './deprecations'; export type { AppCategory } from '../types'; export { DEFAULT_APP_CATEGORIES } from '../utils'; @@ -478,6 +487,8 @@ export interface CoreSetup; } diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 28194a7d0dc3a..34193f8d0c35e 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -29,6 +29,7 @@ import { InternalStatusServiceSetup } from './status'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; +import { InternalDeprecationsServiceSetup } from './deprecations'; /** @internal */ export interface InternalCoreSetup { @@ -45,6 +46,7 @@ export interface InternalCoreSetup { httpResources: InternalHttpResourcesSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; + deprecations: InternalDeprecationsServiceSetup; } /** diff --git a/src/core/server/kibana_config.test.ts b/src/core/server/kibana_config.test.ts index 1c2b268156531..1acdff9dd78e6 100644 --- a/src/core/server/kibana_config.test.ts +++ b/src/core/server/kibana_config.test.ts @@ -22,7 +22,7 @@ const applyKibanaDeprecations = (settings: Record = {}) => { deprecation, path: CONFIG_PATH, })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index d0ff18b381179..97783a7657db5 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -12,12 +12,13 @@ import { ConfigDeprecationProvider } from '@kbn/config'; export type KibanaConfigType = TypeOf; const deprecations: ConfigDeprecationProvider = () => [ - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const kibana = settings[fromPath]; if (kibana?.index) { - log( - `"kibana.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details` - ); + addDeprecation({ + message: `"kibana.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', + }); } return settings; }, diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index da6b521bfde9a..d0a02b9859960 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -7,16 +7,12 @@ */ jest.mock('../../../legacy/server/kbn_server'); -jest.mock('./cli_dev_mode'); import { BehaviorSubject, throwError } from 'rxjs'; import { REPO_ROOT } from '@kbn/dev-utils'; -// @ts-expect-error js file to remove TS dependency on cli -import { CliDevMode as MockCliDevMode } from './cli_dev_mode'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; -import { BasePathProxyServer } from '../http'; import { DiscoveredPlugin } from '../plugins'; import { getEnvOptions, configServiceMock } from '../config/mocks'; @@ -36,6 +32,7 @@ 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; @@ -84,6 +81,7 @@ beforeEach(() => { status: statusServiceMock.createInternalSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), + deprecations: deprecationsServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, uiPlugins: { @@ -228,7 +226,6 @@ describe('once LegacyService is set up with connection info', () => { ); expect(MockKbnServer).not.toHaveBeenCalled(); - expect(MockCliDevMode).not.toHaveBeenCalled(); }); test('reconfigures logging configuration if new config is received.', async () => { @@ -335,74 +332,6 @@ describe('once LegacyService is set up without connection info', () => { }); }); -describe('once LegacyService is set up in `devClusterMaster` mode', () => { - beforeEach(() => { - configService.atPath.mockImplementation((path) => { - return new BehaviorSubject( - path === 'dev' ? { basePathProxyTargetPort: 100500 } : { basePath: '/abc' } - ); - }); - }); - - test('creates CliDevMode without base path proxy.', async () => { - const devClusterLegacyService = new LegacyService({ - coreId, - env: Env.createDefault( - REPO_ROOT, - getEnvOptions({ - cliArgs: { silent: true, basePath: false }, - isDevCliParent: true, - }) - ), - logger, - configService: configService as any, - }); - - await devClusterLegacyService.setupLegacyConfig(); - await devClusterLegacyService.setup(setupDeps); - await devClusterLegacyService.start(startDeps); - - expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledTimes(1); - expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledWith( - expect.objectContaining({ silent: true, basePath: false }), - expect.objectContaining({ - get: expect.any(Function), - set: expect.any(Function), - }), - undefined - ); - }); - - test('creates CliDevMode with base path proxy.', async () => { - const devClusterLegacyService = new LegacyService({ - coreId, - env: Env.createDefault( - REPO_ROOT, - getEnvOptions({ - cliArgs: { quiet: true, basePath: true }, - isDevCliParent: true, - }) - ), - logger, - configService: configService as any, - }); - - await devClusterLegacyService.setupLegacyConfig(); - await devClusterLegacyService.setup(setupDeps); - await devClusterLegacyService.start(startDeps); - - expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledTimes(1); - expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledWith( - expect.objectContaining({ quiet: true, basePath: true }), - expect.objectContaining({ - get: expect.any(Function), - set: expect.any(Function), - }), - expect.any(BasePathProxyServer) - ); - }); -}); - describe('start', () => { test('Cannot start without setup phase', async () => { const legacyService = new LegacyService({ diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 63b84e2461e71..43b348a5ff4a2 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; +import { combineLatest, ConnectableObservable, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, tap } from 'rxjs/operators'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { PathConfigType } from '@kbn/utils'; @@ -18,9 +18,7 @@ import { CoreService } from '../../types'; import { Config } from '../config'; import { CoreContext } from '../core_context'; import { CspConfigType, config as cspConfig } from '../csp'; -import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { - BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig, @@ -64,7 +62,6 @@ export class LegacyService implements CoreService { /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ public readonly legacyId = Symbol(); private readonly log: Logger; - private readonly devConfig$: Observable; private readonly httpConfig$: Observable; private kbnServer?: LegacyKbnServer; private configSubscription?: Subscription; @@ -77,9 +74,6 @@ export class LegacyService implements CoreService { const { logger, configService } = coreContext; this.log = logger.get('legacy-service'); - this.devConfig$ = configService - .atPath(devConfig.path) - .pipe(map((rawConfig) => new DevConfig(rawConfig))); this.httpConfig$ = combineLatest( configService.atPath(httpConfig.path), configService.atPath(cspConfig.path), @@ -142,17 +136,12 @@ export class LegacyService implements CoreService { this.log.debug('starting legacy service'); - // Receive initial config and create kbnServer/ClusterManager. - if (this.coreContext.env.isDevCliParent) { - await this.setupCliDevMode(this.legacyRawConfig!); - } else { - this.kbnServer = await this.createKbnServer( - this.settings!, - this.legacyRawConfig!, - setupDeps, - startDeps - ); - } + this.kbnServer = await this.createKbnServer( + this.settings!, + this.legacyRawConfig!, + setupDeps, + startDeps + ); } public async stop() { @@ -169,26 +158,6 @@ export class LegacyService implements CoreService { } } - private async setupCliDevMode(config: LegacyConfig) { - const basePathProxy$ = this.coreContext.env.cliArgs.basePath - ? combineLatest([this.devConfig$, this.httpConfig$]).pipe( - first(), - map( - ([dev, http]) => - new BasePathProxyServer(this.coreContext.logger.get('server'), http, dev) - ) - ) - : EMPTY; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { CliDevMode } = require('./cli_dev_mode'); - CliDevMode.fromCoreServices( - this.coreContext.env.cliArgs, - config, - await basePathProxy$.toPromise() - ); - } - private async createKbnServer( settings: LegacyVars, config: LegacyConfig, @@ -288,6 +257,11 @@ export class LegacyService implements CoreService { 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, {}]), }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 19056ae1b9bc7..cd0ce7005cc41 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -29,6 +29,7 @@ import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; import { i18nServiceMock } from './i18n/i18n_service.mock'; +import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -49,6 +50,7 @@ export { contextServiceMock } from './context/context_service.mock'; export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; export { i18nServiceMock } from './i18n/i18n_service.mock'; +export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -137,6 +139,7 @@ function createCoreSetupMock({ uiSettings: uiSettingsMock, logging: loggingServiceMock.createSetupContract(), metrics: metricsServiceMock.createSetupContract(), + deprecations: deprecationsServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -174,6 +177,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), + deprecations: deprecationsServiceMock.createInternalSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 74451f38b893e..c466eb2b9ee09 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -165,6 +165,7 @@ export function createPluginSetupContext( register: deps.uiSettings.register, }, getStartServices: () => plugin.startDependencies, + deprecations: deps.deprecations.getRegistry(plugin.name), }; } diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 6a49dd963b4e8..2d54648d22950 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -91,7 +91,7 @@ const createPlugin = ( }); }; -async function testSetup(options: { isDevCliParent?: boolean } = {}) { +async function testSetup() { mockPackage.raw = { branch: 'feature-v1', version: 'v1', @@ -103,10 +103,7 @@ async function testSetup(options: { isDevCliParent?: boolean } = {}) { }; coreId = Symbol('core'); - env = Env.createDefault(REPO_ROOT, { - ...getEnvOptions(), - isDevCliParent: options.isDevCliParent ?? false, - }); + env = Env.createDefault(REPO_ROOT, getEnvOptions()); config$ = new BehaviorSubject>({ plugins: { initialize: true } }); const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); @@ -626,30 +623,3 @@ describe('PluginsService', () => { }); }); }); - -describe('PluginService when isDevCliParent is true', () => { - beforeEach(async () => { - await testSetup({ - isDevCliParent: true, - }); - }); - - describe('#discover()', () => { - it('does not try to run discovery', async () => { - await expect(pluginsService.discover({ environment: environmentSetup })).resolves - .toMatchInlineSnapshot(` - Object { - "pluginPaths": Array [], - "pluginTree": undefined, - "uiPlugins": Object { - "browserConfigs": Map {}, - "internal": Map {}, - "public": Map {}, - }, - } - `); - - expect(mockDiscover).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 92b06d7b6a09b..8b33e2cf4cc6b 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -7,7 +7,7 @@ */ import Path from 'path'; -import { Observable, EMPTY } from 'rxjs'; +import { Observable } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { pick } from '@kbn/std'; @@ -75,11 +75,9 @@ export class PluginsService implements CoreService; private readonly pluginConfigDescriptors = new Map(); private readonly uiPluginInternalInfo = new Map(); - private readonly discoveryDisabled: boolean; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); - this.discoveryDisabled = coreContext.env.isDevCliParent; this.pluginsSystem = new PluginsSystem(coreContext); this.configService = coreContext.configService; this.config$ = coreContext.configService @@ -90,14 +88,9 @@ export class PluginsService implements CoreService(); - const initialize = config.initialize && !this.coreContext.env.isDevCliParent; - if (initialize) { + if (config.initialize) { contracts = await this.pluginsSystem.setupPlugins(deps); this.registerPluginStaticDirs(deps); } else { @@ -131,7 +123,7 @@ export class PluginsService implements CoreService void ) { this.loggingSystem = new LoggingSystem(); @@ -87,10 +87,7 @@ export class Root { // Stream that maps config updates to logger updates, including update failures. const update$ = configService.getConfig$().pipe( // always read the logging config when the underlying config object is re-read - // except for the CLI process where we only apply the default logging config once - switchMap(() => - this.env.isDevCliParent ? of(undefined) : configService.atPath('logging') - ), + switchMap(() => configService.atPath('logging')), concatMap((config) => this.loggingSystem.upgrade(config)), // This specifically console.logs because we were not able to configure the logger. // eslint-disable-next-line no-console diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 22dcc8022858c..468a761781365 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -117,6 +117,7 @@ describe('getSortedObjectsForExport()', () => { "keepAlive": "2m", }, "search": undefined, + "searchAfter": undefined, "sortField": "updated_at", "sortOrder": "desc", "type": Array [ @@ -145,7 +146,7 @@ describe('getSortedObjectsForExport()', () => { type = 'index-pattern', }: { attributes?: Record; - sort?: unknown[]; + sort?: string[]; type?: string; } = {} ) { @@ -461,6 +462,7 @@ describe('getSortedObjectsForExport()', () => { "keepAlive": "2m", }, "search": undefined, + "searchAfter": undefined, "sortField": "updated_at", "sortOrder": "desc", "type": Array [ @@ -617,6 +619,7 @@ describe('getSortedObjectsForExport()', () => { "keepAlive": "2m", }, "search": "foo", + "searchAfter": undefined, "sortField": "updated_at", "sortOrder": "desc", "type": Array [ @@ -710,6 +713,7 @@ describe('getSortedObjectsForExport()', () => { "keepAlive": "2m", }, "search": undefined, + "searchAfter": undefined, "sortField": "updated_at", "sortOrder": "desc", "type": Array [ @@ -808,6 +812,7 @@ describe('getSortedObjectsForExport()', () => { "keepAlive": "2m", }, "search": undefined, + "searchAfter": undefined, "sortField": "updated_at", "sortOrder": "desc", "type": Array [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index c1c0ea73f0bd3..868efa872d643 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -9,7 +9,7 @@ import { createListStream } from '@kbn/utils'; import { PublicMethodsOf } from '@kbn/utility-types'; import { Logger } from '../../logging'; -import { SavedObject, SavedObjectsClientContract, SavedObjectsFindOptions } from '../types'; +import { SavedObject, SavedObjectsClientContract } from '../types'; import { SavedObjectsFindResult } from '../service'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { fetchNestedDependencies } from './fetch_nested_dependencies'; @@ -23,7 +23,6 @@ import { } from './types'; import { SavedObjectsExportError } from './errors'; import { applyExportTransforms } from './apply_export_transforms'; -import { createPointInTimeFinder } from './point_in_time_finder'; import { byIdAscComparator, getPreservedOrderComparator, SavedObjectComparator } from './utils'; /** @@ -168,18 +167,12 @@ export class SavedObjectsExporter { hasReference, search, }: SavedObjectsExportByTypeOptions) { - const findOptions: SavedObjectsFindOptions = { + const finder = this.#savedObjectsClient.createPointInTimeFinder({ type: types, hasReference, hasReferenceOperator: hasReference ? 'OR' : undefined, search, namespaces: namespace ? [namespace] : undefined, - }; - - const finder = createPointInTimeFinder({ - findOptions, - logger: this.#log, - savedObjectsClient: this.#savedObjectsClient, }); const hits: SavedObjectsFindResult[] = []; diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts index d35388ff94749..1952a04ab815c 100644 --- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts @@ -95,7 +95,7 @@ const checkOriginConflict = async ( perPage: 10, fields: ['title'], sortField: 'updated_at', - sortOrder: 'desc', + sortOrder: 'desc' as const, ...(namespace && { namespaces: [namespace] }), }; const findResult = await savedObjectsClient.find<{ title?: string }>(findOptions); diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index fa7531392d122..25fb61de93518 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -102,7 +102,7 @@ export type SavedObjectsFieldMapping = /** @internal */ export interface IndexMapping { - dynamic?: string; + dynamic?: boolean | 'strict'; properties: SavedObjectsMappingProperties; _meta?: IndexMappingMeta; } diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts index 63634bdb1754e..5465da2f620ad 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts @@ -164,6 +164,7 @@ describe('diffMappings', () => { _meta: { migrationMappingPropertyHashes: { foo: 'bar' }, }, + // @ts-expect-error dynamic: 'abcde', properties: {}, }; diff --git a/src/core/server/saved_objects/migrations/core/call_cluster.ts b/src/core/server/saved_objects/migrations/core/call_cluster.ts index bbf39549457d8..f37bbdd14a899 100644 --- a/src/core/server/saved_objects/migrations/core/call_cluster.ts +++ b/src/core/server/saved_objects/migrations/core/call_cluster.ts @@ -12,11 +12,12 @@ * funcationality contained here. */ +import type { estypes } from '@elastic/elasticsearch'; import { IndexMapping } from '../../mappings'; export interface CallCluster { (path: 'bulk', opts: { body: object[] }): Promise; - (path: 'count', opts: CountOpts): Promise<{ count: number; _shards: ShardsInfo }>; + (path: 'count', opts: CountOpts): Promise<{ count: number; _shards: estypes.ShardStatistics }>; (path: 'clearScroll', opts: { scrollId: string }): Promise; (path: 'indices.create', opts: IndexCreationOpts): Promise; (path: 'indices.exists', opts: IndexOpts): Promise; @@ -143,7 +144,7 @@ export interface IndexSettingsResult { } export interface RawDoc { - _id: string; + _id: estypes.Id; _source: any; _type?: string; } @@ -153,14 +154,7 @@ export interface SearchResults { hits: RawDoc[]; }; _scroll_id?: string; - _shards: ShardsInfo; -} - -export interface ShardsInfo { - total: number; - successful: number; - skipped: number; - failed: number; + _shards: estypes.ShardStatistics; } export interface ErrorResponse { diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index bfa686ac0cc47..5cb2a88c4733f 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import _ from 'lodash'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import * as Index from './elastic_index'; @@ -33,41 +34,6 @@ describe('ElasticIndex', () => { expect(client.indices.get).toHaveBeenCalledWith({ index: '.kibana-test' }, { ignore: [404] }); }); - test('fails if the index doc type is unsupported', async () => { - client.indices.get.mockImplementation((params) => { - const index = params!.index as string; - return elasticsearchClientMock.createSuccessTransportRequestPromise({ - [index]: { - aliases: { foo: index }, - mappings: { spock: { dynamic: 'strict', properties: { a: 'b' } } }, - }, - }); - }); - - await expect(Index.fetchInfo(client, '.baz')).rejects.toThrow( - /cannot be automatically migrated/ - ); - }); - - test('fails if there are multiple root types', async () => { - client.indices.get.mockImplementation((params) => { - const index = params!.index as string; - return elasticsearchClientMock.createSuccessTransportRequestPromise({ - [index]: { - aliases: { foo: index }, - mappings: { - doc: { dynamic: 'strict', properties: { a: 'b' } }, - doctor: { dynamic: 'strict', properties: { a: 'b' } }, - }, - }, - }); - }); - - await expect(Index.fetchInfo(client, '.baz')).rejects.toThrow( - /cannot be automatically migrated/ - ); - }); - test('decorates index info with exists and indexName', async () => { client.indices.get.mockImplementation((params) => { const index = params!.index as string; @@ -75,8 +41,9 @@ describe('ElasticIndex', () => { [index]: { aliases: { foo: index }, mappings: { dynamic: 'strict', properties: { a: 'b' } }, + settings: {}, }, - }); + } as estypes.GetIndexResponse); }); const info = await Index.fetchInfo(client, '.baz'); @@ -85,6 +52,7 @@ describe('ElasticIndex', () => { mappings: { dynamic: 'strict', properties: { a: 'b' } }, exists: true, indexName: '.baz', + settings: {}, }); }); }); @@ -134,7 +102,7 @@ describe('ElasticIndex', () => { test('removes existing alias', async () => { client.indices.getAlias.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': '.muchacha', + '.my-fanci-index': { aliases: { '.muchacha': {} } }, }) ); @@ -157,7 +125,7 @@ describe('ElasticIndex', () => { test('allows custom alias actions', async () => { client.indices.getAlias.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': '.muchacha', + '.my-fanci-index': { aliases: { '.muchacha': {} } }, }) ); @@ -185,14 +153,18 @@ describe('ElasticIndex', () => { test('it creates the destination index, then reindexes to it', async () => { client.indices.getAlias.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': '.muchacha', + '.my-fanci-index': { aliases: { '.muchacha': {} } }, }) ); client.reindex.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ task: 'abc' }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + task: 'abc', + } as estypes.ReindexResponse) ); client.tasks.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + completed: true, + } as estypes.GetTaskResponse) ); const info = { @@ -200,7 +172,7 @@ describe('ElasticIndex', () => { exists: true, indexName: '.ze-index', mappings: { - dynamic: 'strict', + dynamic: 'strict' as const, properties: { foo: { type: 'keyword' } }, }, }; @@ -259,13 +231,16 @@ describe('ElasticIndex', () => { test('throws error if re-index task fails', async () => { client.indices.getAlias.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': '.muchacha', + '.my-fanci-index': { aliases: { '.muchacha': {} } }, }) ); client.reindex.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ task: 'abc' }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + task: 'abc', + } as estypes.ReindexResponse) ); client.tasks.get.mockResolvedValue( + // @ts-expect-error @elastic/elasticsearch GetTaskResponse requires a `task` property even on errors elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true, error: { @@ -273,7 +248,7 @@ describe('ElasticIndex', () => { reason: 'all shards failed', failed_shards: [], }, - }) + } as estypes.GetTaskResponse) ); const info = { @@ -286,6 +261,7 @@ describe('ElasticIndex', () => { }, }; + // @ts-expect-error await expect(Index.convertToAlias(client, info, '.muchacha', 10)).rejects.toThrow( /Re-index failed \[search_phase_execution_exception\] all shards failed/ ); @@ -319,7 +295,9 @@ describe('ElasticIndex', () => { describe('write', () => { test('writes documents in bulk to the index', async () => { client.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ items: [] }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + items: [] as any[], + } as estypes.BulkResponse) ); const index = '.myalias'; @@ -356,7 +334,7 @@ describe('ElasticIndex', () => { client.bulk.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ items: [{ index: { error: { type: 'shazm', reason: 'dern' } } }], - }) + } as estypes.BulkResponse) ); const index = '.myalias'; diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index e42643565eb4f..a5f3cb36e736b 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -12,11 +12,12 @@ */ import _ from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { MigrationEsClient } from './migration_es_client'; import { CountResponse, SearchResponse } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; import { SavedObjectsMigrationVersion } from '../../types'; -import { AliasAction, RawDoc, ShardsInfo } from './call_cluster'; +import { AliasAction, RawDoc } from './call_cluster'; import { SavedObjectsRawDocSource } from '../../serialization'; const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; @@ -46,6 +47,7 @@ export async function fetchInfo(client: MigrationEsClient, index: string): Promi const [indexName, indexInfo] = Object.entries(body)[0]; + // @ts-expect-error @elastic/elasticsearch IndexState.alias and IndexState.mappings should be required return assertIsSupportedIndex({ ...indexInfo, exists: true, indexName }); } @@ -142,7 +144,7 @@ export async function write(client: MigrationEsClient, index: string, docs: RawD return; } - const exception: any = new Error(err.index.error!.reason); + const exception: any = new Error(err.index!.error!.reason); exception.detail = err; throw exception; } @@ -322,7 +324,7 @@ function assertIsSupportedIndex(indexInfo: FullIndexInfo) { * Object indices should only ever have a single shard. This is more to handle * instances where customers manually expand the shards of an index. */ -function assertResponseIncludeAllShards({ _shards }: { _shards: ShardsInfo }) { +function assertResponseIncludeAllShards({ _shards }: { _shards: estypes.ShardStatistics }) { if (!_.has(_shards, 'total') || !_.has(_shards, 'successful')) { return; } @@ -375,11 +377,12 @@ async function reindex( await new Promise((r) => setTimeout(r, pollInterval)); const { body } = await client.tasks.get({ - task_id: task, + task_id: String(task), }); - if (body.error) { - const e = body.error; + // @ts-expect-error @elastic/elasticsearch GetTaskResponse doesn't contain `error` property + const e = body.error; + if (e) { throw new Error(`Re-index failed [${e.type}] ${e.reason} :: ${JSON.stringify(e)}`); } diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 0d1939231ce6c..dd295efacf6b8 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -7,6 +7,7 @@ */ import _ from 'lodash'; +import type { estypes } from '@elastic/elasticsearch'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; @@ -443,23 +444,28 @@ function withIndex( elasticsearchClientMock.createSuccessTransportRequestPromise({ task: 'zeid', _shards: { successful: 1, total: 1 }, - }) + } as estypes.ReindexResponse) ); client.tasks.get.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + completed: true, + } as estypes.GetTaskResponse) ); client.search.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult(0)) + elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult(0) as any) ); client.bulk.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ items: [] }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + items: [] as any[], + } as estypes.BulkResponse) ); client.count.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ count: numOutOfDate, _shards: { successful: 1, total: 1 }, - }) + } as estypes.CountResponse) ); + // @ts-expect-error client.scroll.mockImplementation(() => { if (scrollCallCounter <= docs.length) { const result = searchResult(scrollCallCounter); diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 52f155e5d2de2..5bf5ae26f6a0a 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -134,7 +134,7 @@ async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern return; } - const { body: templates } = await client.cat.templates>({ + const { body: templates } = await client.cat.templates({ format: 'json', name: obsoleteIndexTemplatePattern, }); @@ -147,7 +147,7 @@ async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern log.info(`Removing index templates: ${templateNames}`); - return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name }))); + return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name: name! }))); } /** @@ -185,7 +185,13 @@ async function migrateSourceToDest(context: Context) { await Index.write( client, dest.indexName, - await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs, log) + await migrateRawDocs( + serializer, + documentMigrator.migrateAndConvert, + // @ts-expect-error @elastic/elasticsearch `Hit._id` may be a string | number in ES, but we always expect strings in the SO index. + docs, + log + ) ); } } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index b8accc462df9a..7ead37699980a 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -7,13 +7,13 @@ */ import { take } from 'rxjs/operators'; +import { estypes, errors as esErrors } from '@elastic/elasticsearch'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsType } from '../../types'; -import { errors as esErrors } from '@elastic/elasticsearch'; import { DocumentMigrator } from '../core/document_migrator'; jest.mock('../core/document_migrator', () => { return { @@ -105,10 +105,7 @@ describe('KibanaMigrator', () => { const options = mockOptions(); options.client.cat.templates.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { templates: [] }, - { statusCode: 404 } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise([], { statusCode: 404 }) ); options.client.indices.get.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) @@ -129,7 +126,8 @@ describe('KibanaMigrator', () => { options.client.cat.templates.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise( - { templates: [] }, + // @ts-expect-error + { templates: [] } as CatTemplatesResponse, { statusCode: 404 } ) ); @@ -155,7 +153,8 @@ describe('KibanaMigrator', () => { options.client.cat.templates.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise( - { templates: [] }, + // @ts-expect-error + { templates: [] } as CatTemplatesResponse, { statusCode: 404 } ) ); @@ -193,7 +192,8 @@ describe('KibanaMigrator', () => { options.client.cat.templates.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise( - { templates: [] }, + // @ts-expect-error + { templates: [] } as CatTemplatesResponse, { statusCode: 404 } ) ); @@ -323,7 +323,7 @@ describe('KibanaMigrator', () => { completed: true, error: { type: 'elatsicsearch_exception', reason: 'task failed with an error' }, failures: [], - task: { description: 'task description' }, + task: { description: 'task description' } as any, }) ); @@ -365,15 +365,17 @@ const mockV2MigrationOptions = () => { elasticsearchClientMock.createSuccessTransportRequestPromise({ acknowledged: true }) ); options.client.reindex.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ taskId: 'reindex_task_id' }) + elasticsearchClientMock.createSuccessTransportRequestPromise({ + taskId: 'reindex_task_id', + } as estypes.ReindexResponse) ); options.client.tasks.get.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true, error: undefined, failures: [], - task: { description: 'task description' }, - }) + task: { description: 'task description' } as any, + } as estypes.GetTaskResponse) ); return options; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index d025f104c6e3f..22dfb03815052 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -13,9 +13,10 @@ import { ElasticsearchClientError } from '@elastic/elasticsearch/lib/errors'; import { pipe } from 'fp-ts/lib/pipeable'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { flow } from 'fp-ts/lib/function'; +import type { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; -import { SavedObjectsRawDoc } from '../../serialization'; +import { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; import { catchRetryableEsClientErrors, RetryableEsClientError, @@ -56,20 +57,22 @@ export type FetchIndexResponse = Record< export const fetchIndices = ( client: ElasticsearchClient, indicesToFetch: string[] -): TaskEither.TaskEither => () => { - return client.indices - .get( - { - index: indicesToFetch, - ignore_unavailable: true, // Don't return an error for missing indices. Note this *will* include closed indices, the docs are misleading https://github.com/elastic/elasticsearch/issues/63607 - }, - { ignore: [404], maxRetries: 0 } - ) - .then(({ body }) => { - return Either.right(body); - }) - .catch(catchRetryableEsClientErrors); -}; +): TaskEither.TaskEither => + // @ts-expect-error @elastic/elasticsearch IndexState.alias and IndexState.mappings should be required + () => { + return client.indices + .get( + { + index: indicesToFetch, + ignore_unavailable: true, // Don't return an error for missing indices. Note this *will* include closed indices, the docs are misleading https://github.com/elastic/elasticsearch/issues/63607 + }, + { ignore: [404], maxRetries: 0 } + ) + .then(({ body }) => { + return Either.right(body); + }) + .catch(catchRetryableEsClientErrors); + }; /** * Sets a write block in place for the given index. If the response includes @@ -98,7 +101,7 @@ export const setWriteBlock = ( }, { maxRetries: 0 /** handle retry ourselves for now */ } ) - .then((res) => { + .then((res: any) => { return res.body.acknowledged === true ? Either.right('set_write_block_succeeded' as const) : Either.left({ @@ -134,7 +137,11 @@ export const removeWriteBlock = ( // Don't change any existing settings preserve_existing: true, body: { - 'index.blocks.write': false, + index: { + blocks: { + write: false, + }, + }, }, }, { maxRetries: 0 /** handle retry ourselves for now */ } @@ -285,7 +292,7 @@ interface WaitForTaskResponse { error: Option.Option<{ type: string; reason: string; index: string }>; completed: boolean; failures: Option.Option; - description: string; + description?: string; } /** @@ -299,12 +306,7 @@ const waitForTask = ( timeout: string ): TaskEither.TaskEither => () => { return client.tasks - .get<{ - completed: boolean; - response: { failures: any[] }; - task: { description: string }; - error: { type: string; reason: string; index: string }; - }>({ + .get({ task_id: taskId, wait_for_completion: true, timeout, @@ -314,6 +316,7 @@ const waitForTask = ( const failures = body.response?.failures ?? []; return Either.right({ completed: body.completed, + // @ts-expect-error @elastic/elasticsearch GetTaskResponse doesn't declare `error` property error: Option.fromNullable(body.error), failures: failures.length > 0 ? Option.some(failures) : Option.none, description: body.task.description, @@ -359,7 +362,7 @@ export const pickupUpdatedMappings = ( wait_for_completion: false, }) .then(({ body: { task: taskId } }) => { - return Either.right({ taskId }); + return Either.right({ taskId: String(taskId!) }); }) .catch(catchRetryableEsClientErrors); }; @@ -387,7 +390,6 @@ export const reindex = ( .reindex({ // Require targetIndex to be an alias. Prevents a new index from being // created if targetIndex doesn't exist. - // @ts-expect-error This API isn't documented require_alias: requireAlias, body: { // Ignore version conflicts from existing documents @@ -416,7 +418,7 @@ export const reindex = ( wait_for_completion: false, }) .then(({ body: { task: taskId } }) => { - return Either.right({ taskId }); + return Either.right({ taskId: String(taskId) }); }) .catch(catchRetryableEsClientErrors); }; @@ -624,7 +626,7 @@ export const createIndex = ( const aliasesObject = (aliases ?? []).reduce((acc, alias) => { acc[alias] = {}; return acc; - }, {} as Record); + }, {} as Record); return client.indices .create( @@ -727,7 +729,7 @@ export const updateAndPickupMappings = ( 'update_mappings_succeeded' > = () => { return client.indices - .putMapping, IndexMapping>({ + .putMapping({ index, timeout: DEFAULT_TIMEOUT, body: mappings, @@ -774,22 +776,16 @@ export const searchForOutdatedDocuments = ( query: Record ): TaskEither.TaskEither => () => { return client - .search<{ - // when `filter_path` is specified, ES doesn't return empty arrays, so if - // there are no search results res.body.hits will be undefined. - hits?: { - hits?: SavedObjectsRawDoc[]; - }; - }>({ + .search({ index, - // Optimize search performance by sorting by the "natural" index order - sort: ['_doc'], // Return the _seq_no and _primary_term so we can use optimistic // concurrency control for updates seq_no_primary_term: true, size: BATCH_SIZE, body: { query, + // Optimize search performance by sorting by the "natural" index order + sort: ['_doc'], }, // Return an error when targeting missing or closed indices allow_no_indices: false, @@ -811,7 +807,9 @@ export const searchForOutdatedDocuments = ( 'hits.hits._primary_term', ], }) - .then((res) => Either.right({ outdatedDocuments: res.body.hits?.hits ?? [] })) + .then((res) => + Either.right({ outdatedDocuments: (res.body.hits?.hits as SavedObjectsRawDoc[]) ?? [] }) + ) .catch(catchRetryableEsClientErrors); }; @@ -825,20 +823,7 @@ export const bulkOverwriteTransformedDocuments = ( transformedDocs: SavedObjectsRawDoc[] ): TaskEither.TaskEither => () => { return client - .bulk<{ - took: number; - errors: boolean; - items: [ - { - index: { - _id: string; - status: number; - // the filter_path ensures that only items with errors are returned - error: { type: string; reason: string }; - }; - } - ]; - }>({ + .bulk({ // Because we only add aliases in the MARK_VERSION_INDEX_READY step we // can't bulkIndex to an alias with require_alias=true. This means if // users tamper during this operation (delete indices or restore a @@ -880,7 +865,7 @@ export const bulkOverwriteTransformedDocuments = ( // Filter out version_conflict_engine_exception since these just mean // that another instance already updated these documents const errors = (res.body.items ?? []).filter( - (item) => item.index.error.type !== 'version_conflict_engine_exception' + (item) => item.index?.error?.type !== 'version_conflict_engine_exception' ); if (errors.length === 0) { return Either.right('bulk_index_succeeded' as const); 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 46cfd935f429b..2c052a87d028b 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 @@ -258,7 +258,7 @@ describe('migration actions', () => { index: 'clone_red_then_yellow_index', body: { // Enable all shard allocation so that the index status goes yellow - 'index.routing.allocation.enable': 'all', + index: { routing: { allocation: { enable: 'all' } } }, }, }); indexYellow = true; @@ -500,7 +500,7 @@ describe('migration actions', () => { // Create an index with incompatible mappings await createIndex(client, 'reindex_target_6', { - dynamic: 'false', + dynamic: false, properties: { title: { type: 'integer' } }, // integer is incompatible with string title })(); @@ -926,7 +926,7 @@ describe('migration actions', () => { index: 'red_then_yellow_index', body: { // Disable all shard allocation so that the index status is red - 'index.routing.allocation.enable': 'all', + index: { routing: { allocation: { enable: 'all' } } }, }, }); indexYellow = true; 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 95a867934307a..fd62fd107648e 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 @@ -162,7 +162,9 @@ describe('migration v2', () => { const expectedVersions = getExpectedVersionPerType(); const res = await esClient.search({ index: migratedIndex, - sort: ['_doc'], + body: { + sort: ['_doc'], + }, size: 10000, }); const allDocuments = res.body.hits.hits as SavedObjectsRawDoc[]; @@ -217,7 +219,9 @@ describe('migration v2', () => { const expectedVersions = getExpectedVersionPerType(); const res = await esClient.search({ index: migratedIndex, - sort: ['_doc'], + body: { + sort: ['_doc'], + }, size: 10000, }); const allDocuments = res.body.hits.hits as SavedObjectsRawDoc[]; diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 6f915df9dd958..2e92f34429ea9 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -727,7 +727,6 @@ export const createInitialState = ({ }; const reindexTargetMappings: IndexMapping = { - // @ts-expect-error we don't allow plugins to set `dynamic` dynamic: false, properties: { type: { type: 'keyword' }, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index d589809e38f01..52f8dcd310509 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -274,7 +274,7 @@ describe('SavedObjectsService', () => { expect(coreStart.elasticsearch.client.asScoped).toHaveBeenCalledWith(req); const [ - [, , , , includedHiddenTypes], + [, , , , , includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; expect(includedHiddenTypes).toEqual([]); @@ -292,7 +292,7 @@ describe('SavedObjectsService', () => { createScopedRepository(req, ['someHiddenType']); const [ - [, , , , includedHiddenTypes], + [, , , , , includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; expect(includedHiddenTypes).toEqual(['someHiddenType']); @@ -311,7 +311,7 @@ describe('SavedObjectsService', () => { createInternalRepository(); const [ - [, , , client, includedHiddenTypes], + [, , , client, , includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; expect(coreStart.elasticsearch.client.asInternalUser).toBe(client); @@ -328,7 +328,7 @@ describe('SavedObjectsService', () => { createInternalRepository(['someHiddenType']); const [ - [, , , , includedHiddenTypes], + [, , , , , includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; expect(includedHiddenTypes).toEqual(['someHiddenType']); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index fce7f12384456..8e4320eb841f8 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -421,6 +421,7 @@ export class SavedObjectsService this.typeRegistry, kibanaConfig.index, esClient, + this.logger.get('repository'), includedHiddenTypes ); }; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 1186e15cbef4a..8a66e6176d1f5 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -8,6 +8,9 @@ export { SavedObjectsErrorHelpers, SavedObjectsClientProvider, SavedObjectsUtils } from './lib'; export type { SavedObjectsRepository, + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, + SavedObjectsCreatePointInTimeFinderDependencies, ISavedObjectsClientProvider, SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index 16e27bcc12b8f..cef83f103ec53 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -12,7 +12,10 @@ function toArray(value: string | string[]): string[] { /** * Provides an array of paths for ES source filtering */ -export function includedFields(type: string | string[] = '*', fields?: string[] | string) { +export function includedFields( + type: string | string[] = '*', + fields?: string[] | string +): string[] | undefined { if (!fields || fields.length === 0) { return; } diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index d05552bc6e55e..09bce81b14c39 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -8,6 +8,13 @@ export type { ISavedObjectsRepository, SavedObjectsRepository } from './repository'; export { SavedObjectsClientProvider } from './scoped_client_provider'; + +export type { + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, + SavedObjectsCreatePointInTimeFinderDependencies, +} from './point_in_time_finder'; + export type { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, diff --git a/src/core/server/saved_objects/service/lib/point_in_time_finder.mock.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.mock.ts new file mode 100644 index 0000000000000..c689eb319898b --- /dev/null +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.mock.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { loggerMock, MockedLogger } from '../../../logging/logger.mock'; +import type { SavedObjectsClientContract } from '../../types'; +import type { ISavedObjectsRepository } from './repository'; +import { PointInTimeFinder } from './point_in_time_finder'; + +const createPointInTimeFinderMock = ({ + logger = loggerMock.create(), + savedObjectsMock, +}: { + logger?: MockedLogger; + savedObjectsMock: jest.Mocked; +}): jest.Mock => { + const mock = jest.fn(); + + // To simplify testing, we use the actual implementation here, but pass through the + // mocked dependencies. This allows users to set their own `mockResolvedValue` on + // the SO client mock and have it reflected when using `createPointInTimeFinder`. + mock.mockImplementation((findOptions) => { + const finder = new PointInTimeFinder(findOptions, { + logger, + client: savedObjectsMock, + }); + + jest.spyOn(finder, 'find'); + jest.spyOn(finder, 'close'); + + return finder; + }); + + return mock; +}; + +export const savedObjectsPointInTimeFinderMock = { + create: createPointInTimeFinderMock, +}; diff --git a/src/core/server/saved_objects/export/point_in_time_finder.test.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.test.ts similarity index 57% rename from src/core/server/saved_objects/export/point_in_time_finder.test.ts rename to src/core/server/saved_objects/service/lib/point_in_time_finder.test.ts index cd79c7a4b81e5..044bb45269538 100644 --- a/src/core/server/saved_objects/export/point_in_time_finder.test.ts +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.test.ts @@ -6,12 +6,15 @@ * Side Public License, v 1. */ -import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; -import { loggerMock, MockedLogger } from '../../logging/logger.mock'; -import { SavedObjectsFindOptions } from '../types'; -import { SavedObjectsFindResult } from '../service'; +import { loggerMock, MockedLogger } from '../../../logging/logger.mock'; +import type { SavedObjectsClientContract } from '../../types'; +import type { SavedObjectsFindResult } from '../'; +import { savedObjectsRepositoryMock } from './repository.mock'; -import { createPointInTimeFinder } from './point_in_time_finder'; +import { + PointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, +} from './point_in_time_finder'; const mockHits = [ { @@ -40,26 +43,31 @@ const mockHits = [ describe('createPointInTimeFinder()', () => { let logger: MockedLogger; - let savedObjectsClient: ReturnType; + let find: jest.Mocked['find']; + let openPointInTimeForType: jest.Mocked['openPointInTimeForType']; + let closePointInTime: jest.Mocked['closePointInTime']; beforeEach(() => { logger = loggerMock.create(); - savedObjectsClient = savedObjectsClientMock.create(); + const mockRepository = savedObjectsRepositoryMock.create(); + find = mockRepository.find; + openPointInTimeForType = mockRepository.openPointInTimeForType; + closePointInTime = mockRepository.closePointInTime; }); describe('#find', () => { test('throws if a PIT is already open', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'abc123', }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: mockHits, pit_id: 'abc123', per_page: 1, page: 0, }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: mockHits, pit_id: 'abc123', @@ -67,31 +75,38 @@ describe('createPointInTimeFinder()', () => { page: 1, }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', perPage: 1, }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); await finder.find().next(); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - savedObjectsClient.find.mockClear(); + expect(find).toHaveBeenCalledTimes(1); + find.mockClear(); expect(async () => { await finder.find().next(); }).rejects.toThrowErrorMatchingInlineSnapshot( `"Point In Time has already been opened for this finder instance. Please call \`close()\` before calling \`find()\` again."` ); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(0); + expect(find).toHaveBeenCalledTimes(0); }); test('works with a single page of results', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'abc123', }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: mockHits, pit_id: 'abc123', @@ -99,22 +114,29 @@ describe('createPointInTimeFinder()', () => { page: 0, }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); const hits: SavedObjectsFindResult[] = []; for await (const result of finder.find()) { hits.push(...result.saved_objects); } expect(hits.length).toBe(2); - expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect(openPointInTimeForType).toHaveBeenCalledTimes(1); + expect(closePointInTime).toHaveBeenCalledTimes(1); + expect(find).toHaveBeenCalledTimes(1); + expect(find).toHaveBeenCalledWith( expect.objectContaining({ pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }), sortField: 'updated_at', @@ -125,24 +147,24 @@ describe('createPointInTimeFinder()', () => { }); test('works with multiple pages of results', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'abc123', }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: [mockHits[0]], pit_id: 'abc123', per_page: 1, page: 0, }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: [mockHits[1]], pit_id: 'abc123', per_page: 1, page: 0, }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: [], per_page: 1, @@ -150,25 +172,32 @@ describe('createPointInTimeFinder()', () => { page: 0, }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', perPage: 1, }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); const hits: SavedObjectsFindResult[] = []; for await (const result of finder.find()) { hits.push(...result.saved_objects); } expect(hits.length).toBe(2); - expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(openPointInTimeForType).toHaveBeenCalledTimes(1); + expect(closePointInTime).toHaveBeenCalledTimes(1); // called 3 times since we need a 3rd request to check if we // are done paginating through results. - expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); - expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect(find).toHaveBeenCalledTimes(3); + expect(find).toHaveBeenCalledWith( expect.objectContaining({ pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }), sortField: 'updated_at', @@ -181,10 +210,10 @@ describe('createPointInTimeFinder()', () => { describe('#close', () => { test('calls closePointInTime with correct ID', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'test', }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 1, saved_objects: [mockHits[0]], pit_id: 'test', @@ -192,41 +221,48 @@ describe('createPointInTimeFinder()', () => { page: 0, }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', perPage: 2, }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); const hits: SavedObjectsFindResult[] = []; for await (const result of finder.find()) { hits.push(...result.saved_objects); await finder.close(); } - expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test'); + expect(closePointInTime).toHaveBeenCalledWith('test'); }); test('causes generator to stop', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'test', }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: [mockHits[0]], pit_id: 'test', per_page: 1, page: 0, }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: [mockHits[1]], pit_id: 'test', per_page: 1, page: 0, }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: [], per_page: 1, @@ -234,36 +270,50 @@ describe('createPointInTimeFinder()', () => { page: 0, }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', perPage: 1, }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); const hits: SavedObjectsFindResult[] = []; for await (const result of finder.find()) { hits.push(...result.saved_objects); await finder.close(); } - expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(closePointInTime).toHaveBeenCalledTimes(1); expect(hits.length).toBe(1); }); test('is called if `find` throws an error', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'test', }); - savedObjectsClient.find.mockRejectedValueOnce(new Error('oops')); + find.mockRejectedValueOnce(new Error('oops')); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', perPage: 2, }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); const hits: SavedObjectsFindResult[] = []; try { for await (const result of finder.find()) { @@ -273,21 +323,21 @@ describe('createPointInTimeFinder()', () => { // intentionally empty } - expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test'); + expect(closePointInTime).toHaveBeenCalledWith('test'); }); test('finder can be reused after closing', async () => { - savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + openPointInTimeForType.mockResolvedValueOnce({ id: 'abc123', }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: mockHits, pit_id: 'abc123', per_page: 1, page: 0, }); - savedObjectsClient.find.mockResolvedValueOnce({ + find.mockResolvedValueOnce({ total: 2, saved_objects: mockHits, pit_id: 'abc123', @@ -295,13 +345,20 @@ describe('createPointInTimeFinder()', () => { page: 1, }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: ['visualization'], search: 'foo*', perPage: 1, }; - const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const finder = new PointInTimeFinder(findOptions, { + logger, + client: { + find, + openPointInTimeForType, + closePointInTime, + }, + }); const findA = finder.find(); await findA.next(); @@ -313,9 +370,9 @@ describe('createPointInTimeFinder()', () => { expect((await findA.next()).done).toBe(true); expect((await findB.next()).done).toBe(true); - expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(2); + expect(openPointInTimeForType).toHaveBeenCalledTimes(2); + expect(find).toHaveBeenCalledTimes(2); + expect(closePointInTime).toHaveBeenCalledTimes(2); }); }); }); diff --git a/src/core/server/saved_objects/export/point_in_time_finder.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts similarity index 52% rename from src/core/server/saved_objects/export/point_in_time_finder.ts rename to src/core/server/saved_objects/service/lib/point_in_time_finder.ts index dc0bac6b6bfd9..9a8dcceafebb2 100644 --- a/src/core/server/saved_objects/export/point_in_time_finder.ts +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts @@ -5,80 +5,77 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; +import type { Logger } from '../../../logging'; +import type { SavedObjectsFindOptions, SavedObjectsClientContract } from '../../types'; +import type { SavedObjectsFindResponse } from '../'; -import { Logger } from '../../logging'; -import { SavedObjectsClientContract, SavedObjectsFindOptions } from '../types'; -import { SavedObjectsFindResponse } from '../service'; +type PointInTimeFinderClient = Pick< + SavedObjectsClientContract, + 'find' | 'openPointInTimeForType' | 'closePointInTime' +>; /** - * Returns a generator to help page through large sets of saved objects. - * - * The generator wraps calls to `SavedObjects.find` and iterates over - * multiple pages of results using `_pit` and `search_after`. This will - * open a new Point In Time (PIT), and continue paging until a set of - * results is received that's smaller than the designated `perPage`. - * - * Once you have retrieved all of the results you need, it is recommended - * to call `close()` to clean up the PIT and prevent Elasticsearch from - * consuming resources unnecessarily. This will automatically be done for - * you if you reach the last page of results. - * - * @example - * ```ts - * const findOptions: SavedObjectsFindOptions = { - * type: 'visualization', - * search: 'foo*', - * perPage: 100, - * }; - * - * const finder = createPointInTimeFinder({ - * logger, - * savedObjectsClient, - * findOptions, - * }); - * - * const responses: SavedObjectFindResponse[] = []; - * for await (const response of finder.find()) { - * responses.push(...response); - * if (doneSearching) { - * await finder.close(); - * } - * } - * ``` + * @public */ -export function createPointInTimeFinder({ - findOptions, - logger, - savedObjectsClient, -}: { - findOptions: SavedObjectsFindOptions; +export type SavedObjectsCreatePointInTimeFinderOptions = Omit< + SavedObjectsFindOptions, + 'page' | 'pit' | 'searchAfter' +>; + +/** + * @public + */ +export interface SavedObjectsCreatePointInTimeFinderDependencies { + client: Pick; +} + +/** + * @internal + */ +export interface PointInTimeFinderDependencies + extends SavedObjectsCreatePointInTimeFinderDependencies { logger: Logger; - savedObjectsClient: SavedObjectsClientContract; -}) { - return new PointInTimeFinder({ findOptions, logger, savedObjectsClient }); +} + +/** @public */ +export interface ISavedObjectsPointInTimeFinder { + /** + * An async generator which wraps calls to `savedObjectsClient.find` and + * iterates over multiple pages of results using `_pit` and `search_after`. + * This will open a new Point-In-Time (PIT), and continue paging until a set + * of results is received that's smaller than the designated `perPage` size. + */ + find: () => AsyncGenerator; + /** + * Closes the Point-In-Time associated with this finder instance. + * + * Once you have retrieved all of the results you need, it is recommended + * to call `close()` to clean up the PIT and prevent Elasticsearch from + * consuming resources unnecessarily. This is only required if you are + * done iterating and have not yet paged through all of the results: the + * PIT will automatically be closed for you once you reach the last page + * of results, or if the underlying call to `find` fails for any reason. + */ + close: () => Promise; } /** * @internal */ -export class PointInTimeFinder { +export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { readonly #log: Logger; - readonly #savedObjectsClient: SavedObjectsClientContract; + readonly #client: PointInTimeFinderClient; readonly #findOptions: SavedObjectsFindOptions; #open: boolean = false; #pitId?: string; - constructor({ - findOptions, - logger, - savedObjectsClient, - }: { - findOptions: SavedObjectsFindOptions; - logger: Logger; - savedObjectsClient: SavedObjectsClientContract; - }) { - this.#log = logger; - this.#savedObjectsClient = savedObjectsClient; + constructor( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + { logger, client }: PointInTimeFinderDependencies + ) { + this.#log = logger.get('point-in-time-finder'); + this.#client = client; this.#findOptions = { // Default to 1000 items per page as a tradeoff between // speed and memory consumption. @@ -99,18 +96,18 @@ export class PointInTimeFinder { await this.open(); let lastResultsCount: number; - let lastHitSortValue: unknown[] | undefined; + let lastHitSortValue: estypes.Id[] | undefined; do { const results = await this.findNext({ findOptions: this.#findOptions, id: this.#pitId, - ...(lastHitSortValue ? { searchAfter: lastHitSortValue } : {}), + searchAfter: lastHitSortValue, }); this.#pitId = results.pit_id; lastResultsCount = results.saved_objects.length; lastHitSortValue = this.getLastHitSortValue(results); - this.#log.debug(`Collected [${lastResultsCount}] saved objects for export.`); + this.#log.debug(`Collected [${lastResultsCount}] saved objects`); // Close PIT if this was our last page if (this.#pitId && lastResultsCount < this.#findOptions.perPage!) { @@ -129,7 +126,7 @@ export class PointInTimeFinder { try { if (this.#pitId) { this.#log.debug(`Closing PIT for types [${this.#findOptions.type}]`); - await this.#savedObjectsClient.closePointInTime(this.#pitId); + await this.#client.closePointInTime(this.#pitId); this.#pitId = undefined; } this.#open = false; @@ -141,13 +138,14 @@ export class PointInTimeFinder { private async open() { try { - const { id } = await this.#savedObjectsClient.openPointInTimeForType(this.#findOptions.type); + const { id } = await this.#client.openPointInTimeForType(this.#findOptions.type); this.#pitId = id; this.#open = true; } catch (e) { - // Since `find` swallows 404s, it is expected that exporter will do the same, + // Since `find` swallows 404s, it is expected that finder will do the same, // so we only rethrow non-404 errors here. - if (e.output.statusCode !== 404) { + if (e.output?.statusCode !== 404) { + this.#log.error(`Failed to open PIT for types [${this.#findOptions.type}]`); throw e; } this.#log.debug(`Unable to open PIT for types [${this.#findOptions.type}]: 404 ${e}`); @@ -161,17 +159,17 @@ export class PointInTimeFinder { }: { findOptions: SavedObjectsFindOptions; id?: string; - searchAfter?: unknown[]; + searchAfter?: estypes.Id[]; }) { try { - return await this.#savedObjectsClient.find({ + return await this.#client.find({ // Sort fields are required to use searchAfter, so we set some defaults here sortField: 'updated_at', sortOrder: 'desc', // Bump keep_alive by 2m on every new request to allow for the ES client // to make multiple retries in the event of a network failure. - ...(id ? { pit: { id, keepAlive: '2m' } } : {}), - ...(searchAfter ? { searchAfter } : {}), + pit: id ? { id, keepAlive: '2m' } : undefined, + searchAfter, ...findOptions, }); } catch (e) { @@ -183,7 +181,7 @@ export class PointInTimeFinder { } } - private getLastHitSortValue(res: SavedObjectsFindResponse): unknown[] | undefined { + private getLastHitSortValue(res: SavedObjectsFindResponse): estypes.Id[] | undefined { if (res.saved_objects.length < 1) { return undefined; } diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index a3610b1e437e2..a2092e0571808 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -6,26 +6,36 @@ * Side Public License, v 1. */ +import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; import { ISavedObjectsRepository } from './repository'; -const create = (): jest.Mocked => ({ - checkConflicts: jest.fn(), - create: jest.fn(), - bulkCreate: jest.fn(), - bulkUpdate: jest.fn(), - delete: jest.fn(), - bulkGet: jest.fn(), - find: jest.fn(), - get: jest.fn(), - closePointInTime: jest.fn(), - openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), - resolve: jest.fn(), - update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), - deleteByNamespace: jest.fn(), - incrementCounter: jest.fn(), - removeReferencesTo: jest.fn(), -}); +const create = () => { + const mock: jest.Mocked = { + checkConflicts: jest.fn(), + create: jest.fn(), + bulkCreate: jest.fn(), + bulkUpdate: jest.fn(), + delete: jest.fn(), + bulkGet: jest.fn(), + find: jest.fn(), + get: jest.fn(), + closePointInTime: jest.fn(), + createPointInTimeFinder: jest.fn(), + openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), + resolve: jest.fn(), + update: jest.fn(), + addToNamespaces: jest.fn(), + deleteFromNamespaces: jest.fn(), + deleteByNamespace: jest.fn(), + incrementCounter: jest.fn(), + removeReferencesTo: jest.fn(), + }; + + mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ + savedObjectsMock: mock, + }); + + return mock; +}; export const savedObjectsRepositoryMock = { create }; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index d26d92e84925a..ce48e8dc9a317 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -6,10 +6,14 @@ * Side Public License, v 1. */ +import { pointInTimeFinderMock } from './repository.test.mock'; + import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; import { SavedObjectsErrorHelpers } from './errors'; +import { PointInTimeFinder } from './point_in_time_finder'; import { ALL_NAMESPACES_STRING } from './utils'; +import { loggerMock } from '../../../logging/logger.mock'; import { SavedObjectsSerializer } from '../../serialization'; import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; @@ -19,6 +23,7 @@ import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { esKuery } from '../../es_query'; import { errors as EsErrors } from '@elastic/elasticsearch'; + const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -39,6 +44,7 @@ describe('SavedObjectsRepository', () => { let client; let savedObjectsRepository; let migrator; + let logger; let serializer; const mockTimestamp = '2017-08-14T15:49:14.886Z'; @@ -238,11 +244,13 @@ describe('SavedObjectsRepository', () => { }; beforeEach(() => { + pointInTimeFinderMock.mockClear(); client = elasticsearchClientMock.createElasticsearchClient(); migrator = mockKibanaMigrator.create(); documentMigrator.prepareMigrations(); migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate); migrator.runMigrations = async () => ({ status: 'skipped' }); + logger = loggerMock.create(); // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation serializer = { @@ -269,6 +277,7 @@ describe('SavedObjectsRepository', () => { typeRegistry: registry, serializer, allowedTypes, + logger, }); savedObjectsRepository._getCurrentTime = jest.fn(() => mockTimestamp); @@ -2774,18 +2783,20 @@ describe('SavedObjectsRepository', () => { await findSuccess({ type, fields: ['title'] }); expect(client.search).toHaveBeenCalledWith( expect.objectContaining({ - _source: [ - `${type}.title`, - 'namespace', - 'namespaces', - 'type', - 'references', - 'migrationVersion', - 'coreMigrationVersion', - 'updated_at', - 'originId', - 'title', - ], + body: expect.objectContaining({ + _source: [ + `${type}.title`, + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'coreMigrationVersion', + 'updated_at', + 'originId', + 'title', + ], + }), }), expect.anything() ); @@ -3644,6 +3655,33 @@ describe('SavedObjectsRepository', () => { ); }); + it(`uses the 'upsertAttributes' option when specified`, async () => { + const upsertAttributes = { + foo: 'bar', + hello: 'dolly', + }; + await incrementCounterSuccess(type, id, counterFields, { namespace, upsertAttributes }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + upsert: expect.objectContaining({ + [type]: { + foo: 'bar', + hello: 'dolly', + ...counterFields.reduce((aggs, field) => { + return { + ...aggs, + [field]: 1, + }; + }, {}), + }, + }), + }), + }), + expect.anything() + ); + }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( @@ -3829,6 +3867,7 @@ describe('SavedObjectsRepository', () => { id: '6.0.0-alpha1', ...mockTimestampFields, version: mockVersion, + references: [], attributes: { buildNum: 8468, apiCallsCount: 100, @@ -4632,4 +4671,31 @@ describe('SavedObjectsRepository', () => { }); }); }); + + describe('#createPointInTimeFinder', () => { + it('returns a new PointInTimeFinder instance', async () => { + const result = await savedObjectsRepository.createPointInTimeFinder({}, {}); + expect(result).toBeInstanceOf(PointInTimeFinder); + }); + + it('calls PointInTimeFinder with the provided options and dependencies', async () => { + const options = Symbol(); + const dependencies = { + client: { + find: Symbol(), + openPointInTimeForType: Symbol(), + closePointInTime: Symbol(), + }, + }; + + await savedObjectsRepository.createPointInTimeFinder(options, dependencies); + expect(pointInTimeFinderMock).toHaveBeenCalledWith( + options, + expect.objectContaining({ + ...dependencies, + logger, + }) + ); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.test.mock.ts b/src/core/server/saved_objects/service/lib/repository.test.mock.ts new file mode 100644 index 0000000000000..3eba77b465819 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/repository.test.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const pointInTimeFinderMock = jest.fn(); +jest.doMock('./point_in_time_finder', () => ({ + PointInTimeFinder: pointInTimeFinderMock, +})); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 7a54cdb8488d8..6e2a1d6ec0511 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -7,13 +7,16 @@ */ import { omit, isObject } from 'lodash'; -import { - ElasticsearchClient, - DeleteDocumentResponse, - GetResponse, - SearchResponse, -} from '../../../elasticsearch/'; +import type { estypes } from '@elastic/elasticsearch'; +import type { ElasticsearchClient } from '../../../elasticsearch/'; +import type { Logger } from '../../../logging'; import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; +import { + ISavedObjectsPointInTimeFinder, + PointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, + SavedObjectsCreatePointInTimeFinderDependencies, +} from './point_in_time_finder'; import { createRepositoryEsClient, RepositoryEsClient } from './repository_es_client'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; @@ -73,10 +76,16 @@ import { // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Left = { tag: 'Left'; error: Record }; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Right = { tag: 'Right'; value: Record }; +interface Left { + tag: 'Left'; + error: Record; +} + +interface Right { + tag: 'Right'; + value: Record; +} + type Either = Left | Right; const isLeft = (either: Either): either is Left => either.tag === 'Left'; const isRight = (either: Either): either is Right => either.tag === 'Right'; @@ -89,12 +98,14 @@ export interface SavedObjectsRepositoryOptions { serializer: SavedObjectsSerializer; migrator: IKibanaMigrator; allowedTypes: string[]; + logger: Logger; } /** * @public */ -export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { +export interface SavedObjectsIncrementCounterOptions + extends SavedObjectsBaseOptions { /** * (default=false) If true, sets all the counter fields to 0 if they don't * already exist. Existing fields will be left as-is and won't be incremented. @@ -107,6 +118,10 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt * operation. See {@link MutatingOperationRefreshSetting} */ refresh?: MutatingOperationRefreshSetting; + /** + * Attributes to use when upserting the document if it doesn't exist. + */ + upsertAttributes?: Attributes; } /** @@ -148,6 +163,7 @@ export class SavedObjectsRepository { private _allowedTypes: string[]; private readonly client: RepositoryEsClient; private _serializer: SavedObjectsSerializer; + private _logger: Logger; /** * A factory function for creating SavedObjectRepository instances. @@ -162,6 +178,7 @@ export class SavedObjectsRepository { typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, + logger: Logger, includedHiddenTypes: string[] = [], injectedConstructor: any = SavedObjectsRepository ): ISavedObjectsRepository { @@ -187,6 +204,7 @@ export class SavedObjectsRepository { serializer, allowedTypes, client, + logger, }); } @@ -199,6 +217,7 @@ export class SavedObjectsRepository { serializer, migrator, allowedTypes = [], + logger, } = options; // It's important that we migrate documents / mark them as up-to-date @@ -218,6 +237,7 @@ export class SavedObjectsRepository { } this._allowedTypes = allowedTypes; this._serializer = serializer; + this._logger = logger; } /** @@ -384,7 +404,7 @@ export class SavedObjectsRepository { _source: ['type', 'namespaces'], })); const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget( + ? await this.client.mget( { body: { docs: bulkGetDocs, @@ -412,8 +432,9 @@ export class SavedObjectsRepository { if (esRequestIndex !== undefined) { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; - const docFound = indexFound && actualResult.found === true; - if (docFound && !this.rawDocExistsInNamespace(actualResult, namespace)) { + const docFound = indexFound && actualResult?.found === true; + // @ts-expect-error MultiGetHit._source is optional + if (docFound && !this.rawDocExistsInNamespace(actualResult!, namespace)) { const { id, type } = object; return { tag: 'Left' as 'Left', @@ -428,7 +449,10 @@ export class SavedObjectsRepository { }; } savedObjectNamespaces = - initialNamespaces || getSavedObjectNamespaces(namespace, docFound && actualResult); + initialNamespaces || + // @ts-expect-error MultiGetHit._source is optional + getSavedObjectNamespaces(namespace, docFound ? actualResult : undefined); + // @ts-expect-error MultiGetHit._source is optional versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(object.type)) { @@ -487,7 +511,7 @@ export class SavedObjectsRepository { const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; const { error, ...rawResponse } = Object.values( - bulkResponse?.body.items[esRequestIndex] + bulkResponse?.body.items[esRequestIndex] ?? {} )[0] as any; if (error) { @@ -551,10 +575,10 @@ export class SavedObjectsRepository { const bulkGetDocs = expectedBulkGetResults.filter(isRight).map(({ value: { type, id } }) => ({ _id: this._serializer.generateRawId(namespace, type, id), _index: this.getIndexForType(type), - _source: ['type', 'namespaces'], + _source: { includes: ['type', 'namespaces'] }, })); const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget( + ? await this.client.mget( { body: { docs: bulkGetDocs, @@ -573,13 +597,14 @@ export class SavedObjectsRepository { const { type, id, esRequestIndex } = expectedResult.value; const doc = bulkGetResponse?.body.docs[esRequestIndex]; - if (doc.found) { + if (doc?.found) { errors.push({ id, type, error: { ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), - ...(!this.rawDocExistsInNamespace(doc, namespace) && { + // @ts-expect-error MultiGetHit._source is optional + ...(!this.rawDocExistsInNamespace(doc!, namespace) && { metadata: { isNotOverwritable: true }, }), }, @@ -623,7 +648,7 @@ export class SavedObjectsRepository { } } - const { body, statusCode } = await this.client.delete( + const { body, statusCode } = await this.client.delete( { id: rawId, index: this.getIndexForType(type), @@ -639,6 +664,7 @@ export class SavedObjectsRepository { } const deleteDocNotFound = body.result === 'not_found'; + // @ts-expect-error 'error' does not exist on type 'DeleteResponse' const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; if (deleteDocNotFound || deleteIndexNotFound) { // see "404s from missing index" above @@ -800,15 +826,18 @@ export class SavedObjectsRepository { const esOptions = { // If `pit` is provided, we drop the `index`, otherwise ES returns 400. - ...(pit ? {} : { index: this.getIndicesForTypes(allowedTypes) }), + index: pit ? undefined : this.getIndicesForTypes(allowedTypes), // If `searchAfter` is provided, we drop `from` as it will not be used for pagination. - ...(searchAfter ? {} : { from: perPage * (page - 1) }), + from: searchAfter ? undefined : perPage * (page - 1), _source: includedFields(type, fields), preference, rest_total_hits_as_int: true, size: perPage, body: { + size: perPage, seq_no_primary_term: true, + from: perPage * (page - 1), + _source: includedFields(type, fields), ...getSearchDsl(this._mappings, this._registry, { search, defaultSearchOperator, @@ -828,7 +857,7 @@ export class SavedObjectsRepository { }, }; - const { body, statusCode } = await this.client.search>(esOptions, { + const { body, statusCode } = await this.client.search(esOptions, { ignore: [404], }); if (statusCode === 404) { @@ -847,13 +876,15 @@ export class SavedObjectsRepository { per_page: perPage, total: body.hits.total, saved_objects: body.hits.hits.map( - (hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({ + (hit: estypes.Hit): SavedObjectsFindResult => ({ + // @ts-expect-error @elastic/elasticsearch declared Id as string | number ...this._rawToSavedObject(hit), - score: (hit as any)._score, - ...((hit as any).sort && { sort: (hit as any).sort }), + score: hit._score!, + // @ts-expect-error @elastic/elasticsearch declared sort as string | number + sort: hit.sort, }) ), - ...(body.pit_id && { pit_id: body.pit_id }), + pit_id: body.pit_id, } as SavedObjectsFindResponse; } @@ -912,10 +943,10 @@ export class SavedObjectsRepository { .map(({ value: { type, id, fields } }) => ({ _id: this._serializer.generateRawId(namespace, type, id), _index: this.getIndexForType(type), - _source: includedFields(type, fields), + _source: { includes: includedFields(type, fields) }, })); const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget( + ? await this.client.mget( { body: { docs: bulkGetDocs, @@ -934,7 +965,8 @@ export class SavedObjectsRepository { const { type, id, esRequestIndex } = expectedResult.value; const doc = bulkGetResponse?.body.docs[esRequestIndex]; - if (!doc.found || !this.rawDocExistsInNamespace(doc, namespace)) { + // @ts-expect-error MultiGetHit._source is optional + if (!doc?.found || !this.rawDocExistsInNamespace(doc, namespace)) { return ({ id, type, @@ -942,6 +974,7 @@ export class SavedObjectsRepository { } as any) as SavedObject; } + // @ts-expect-error MultiGetHit._source is optional return this.getSavedObjectFromSource(type, id, doc); }), }; @@ -967,7 +1000,7 @@ export class SavedObjectsRepository { const namespace = normalizeNamespace(options.namespace); - const { body, statusCode } = await this.client.get>( + const { body, statusCode } = await this.client.get( { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), @@ -975,9 +1008,13 @@ export class SavedObjectsRepository { { ignore: [404] } ); - const docNotFound = body.found === false; const indexNotFound = statusCode === 404; - if (docNotFound || indexNotFound || !this.rawDocExistsInNamespace(body, namespace)) { + + if ( + !isFoundGetResponse(body) || + indexNotFound || + !this.rawDocExistsInNamespace(body, namespace) + ) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -1013,7 +1050,7 @@ export class SavedObjectsRepository { const time = this._getCurrentTime(); // retrieve the alias, and if it is not disabled, update it - const aliasResponse = await this.client.update( + const aliasResponse = await this.client.update<{ 'legacy-url-alias': LegacyUrlAlias }>( { id: rawAliasId, index: this.getIndexForType(LEGACY_URL_ALIAS_TYPE), @@ -1046,15 +1083,16 @@ export class SavedObjectsRepository { if ( aliasResponse.statusCode === 404 || - aliasResponse.body.get.found === false || - aliasResponse.body.get._source[LEGACY_URL_ALIAS_TYPE]?.disabled === true + aliasResponse.body.get?.found === false || + aliasResponse.body.get?._source[LEGACY_URL_ALIAS_TYPE]?.disabled === true ) { // no legacy URL alias exists, or one exists but it's disabled; just attempt to get the object return this.resolveExactMatch(type, id, options); } - const legacyUrlAlias: LegacyUrlAlias = aliasResponse.body.get._source[LEGACY_URL_ALIAS_TYPE]; + + const legacyUrlAlias: LegacyUrlAlias = aliasResponse.body.get!._source[LEGACY_URL_ALIAS_TYPE]; const objectIndex = this.getIndexForType(type); - const bulkGetResponse = await this.client.mget( + const bulkGetResponse = await this.client.mget( { body: { docs: [ @@ -1077,23 +1115,28 @@ export class SavedObjectsRepository { const exactMatchDoc = bulkGetResponse?.body.docs[0]; const aliasMatchDoc = bulkGetResponse?.body.docs[1]; const foundExactMatch = + // @ts-expect-error MultiGetHit._source is optional exactMatchDoc.found && this.rawDocExistsInNamespace(exactMatchDoc, namespace); const foundAliasMatch = + // @ts-expect-error MultiGetHit._source is optional aliasMatchDoc.found && this.rawDocExistsInNamespace(aliasMatchDoc, namespace); if (foundExactMatch && foundAliasMatch) { return { + // @ts-expect-error MultiGetHit._source is optional saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), outcome: 'conflict', aliasTargetId: legacyUrlAlias.targetId, }; } else if (foundExactMatch) { return { + // @ts-expect-error MultiGetHit._source is optional saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), outcome: 'exactMatch', }; } else if (foundAliasMatch) { return { + // @ts-expect-error MultiGetHit._source is optional saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc), outcome: 'aliasMatch', aliasTargetId: legacyUrlAlias.targetId, @@ -1140,7 +1183,7 @@ export class SavedObjectsRepository { }; const { body } = await this.client - .update({ + .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), ...getExpectedVersionProperties(version, preflightResult), @@ -1160,11 +1203,11 @@ export class SavedObjectsRepository { throw err; }); - const { originId } = body.get._source; - let namespaces = []; + const { originId } = body.get?._source ?? {}; + let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body.get._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(body.get._source.namespace), + namespaces = body.get?._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(body.get?._source.namespace), ]; } @@ -1172,7 +1215,6 @@ export class SavedObjectsRepository { id, type, updated_at: time, - // @ts-expect-error update doesn't have _seq_no, _primary_term as Record / any in LP version: encodeHitVersion(body), namespaces, ...(originId && { originId }), @@ -1312,7 +1354,7 @@ export class SavedObjectsRepository { return { namespaces: doc.namespaces }; } else { // if there are no namespaces remaining, delete the saved object - const { body, statusCode } = await this.client.delete( + const { body, statusCode } = await this.client.delete( { id: this._serializer.generateRawId(undefined, type, id), refresh, @@ -1330,6 +1372,7 @@ export class SavedObjectsRepository { } const deleteDocNotFound = body.result === 'not_found'; + // @ts-expect-error const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; if (deleteDocNotFound || deleteIndexNotFound) { // see "404s from missing index" above @@ -1464,9 +1507,10 @@ export class SavedObjectsRepository { if (esRequestIndex !== undefined) { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; - const docFound = indexFound && actualResult.found === true; + const docFound = indexFound && actualResult?.found === true; if ( !docFound || + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source !this.rawDocExistsInNamespace(actualResult, getNamespaceId(objectNamespace)) ) { return { @@ -1478,10 +1522,13 @@ export class SavedObjectsRepository { }, }; } - namespaces = actualResult._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(actualResult._source.namespace), + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source + namespaces = actualResult!._source.namespaces ?? [ + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source + SavedObjectsUtils.namespaceIdToString(actualResult!._source.namespace), ]; - versionProperties = getExpectedVersionProperties(version, actualResult); + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source + versionProperties = getExpectedVersionProperties(version, actualResult!); } else { if (this._registry.isSingleNamespace(type)) { // if `objectNamespace` is undefined, fall back to `options.namespace` @@ -1530,7 +1577,7 @@ export class SavedObjectsRepository { } const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; - const response = bulkUpdateResponse?.body.items[esRequestIndex]; + const response = bulkUpdateResponse?.body.items[esRequestIndex] ?? {}; // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values( @@ -1623,7 +1670,7 @@ export class SavedObjectsRepository { } return { - updated: body.updated, + updated: body.updated!, }; } @@ -1658,6 +1705,20 @@ export class SavedObjectsRepository { * .incrementCounter('dashboard_counter_type', 'counter_id', [ * 'stats.apiCalls', * ]) + * + * // Increment the apiCalls field counter by 4 + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * { fieldName: 'stats.apiCalls' incrementBy: 4 }, + * ]) + * + * // Initialize the document with arbitrary fields if not present + * repository.incrementCounter<{ appId: string }>( + * 'dashboard_counter_type', + * 'counter_id', + * [ 'stats.apiCalls'], + * { upsertAttributes: { appId: 'myId' } } + * ) * ``` * * @param type - The type of saved object whose fields should be incremented @@ -1670,7 +1731,7 @@ export class SavedObjectsRepository { type: string, id: string, counterFields: Array, - options: SavedObjectsIncrementCounterOptions = {} + options: SavedObjectsIncrementCounterOptions = {} ): Promise> { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); @@ -1692,12 +1753,16 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options; + const { + migrationVersion, + refresh = DEFAULT_REFRESH_SETTING, + initialize = false, + upsertAttributes, + } = options; const normalizedCounterFields = counterFields.map((counterField) => { const fieldName = typeof counterField === 'string' ? counterField : counterField.fieldName; const incrementBy = typeof counterField === 'string' ? 1 : counterField.incrementBy || 1; - return { fieldName, incrementBy: initialize ? 0 : incrementBy, @@ -1721,18 +1786,21 @@ export class SavedObjectsRepository { type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: normalizedCounterFields.reduce((acc, counterField) => { - const { fieldName, incrementBy } = counterField; - acc[fieldName] = incrementBy; - return acc; - }, {} as Record), + attributes: { + ...(upsertAttributes ?? {}), + ...normalizedCounterFields.reduce((acc, counterField) => { + const { fieldName, incrementBy } = counterField; + acc[fieldName] = incrementBy; + return acc; + }, {} as Record), + }, migrationVersion, updated_at: time, }); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - const { body } = await this.client.update({ + const { body } = await this.client.update({ id: raw._id, index: this.getIndexForType(type), refresh, @@ -1770,17 +1838,16 @@ export class SavedObjectsRepository { }, }); - const { originId } = body.get._source; + const { originId } = body.get?._source ?? {}; return { id, type, ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), ...(originId && { originId }), updated_at: time, - references: body.get._source.references, - // @ts-expect-error + references: body.get?._source.references ?? [], version: encodeHitVersion(body), - attributes: body.get._source[type], + attributes: body.get?._source[type], }; } @@ -1788,6 +1855,9 @@ export class SavedObjectsRepository { * Opens a Point In Time (PIT) against the indices for the specified Saved Object types. * The returned `id` can then be passed to `SavedObjects.find` to search against that PIT. * + * Only use this API if you have an advanced use case that's not solved by the + * {@link SavedObjectsRepository.createPointInTimeFinder} method. + * * @example * ```ts * const { id } = await savedObjectsClient.openPointInTimeForType( @@ -1836,9 +1906,13 @@ export class SavedObjectsRepository { const { body, statusCode, - } = await this.client.openPointInTime(esOptions, { - ignore: [404], - }); + } = await this.client.openPointInTime( + // @ts-expect-error @elastic/elasticsearch OpenPointInTimeRequest.index expected to accept string[] + esOptions, + { + ignore: [404], + } + ); if (statusCode === 404) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(); } @@ -1853,6 +1927,9 @@ export class SavedObjectsRepository { * via the Elasticsearch client, and is included in the Saved Objects Client * as a convenience for consumers who are using `openPointInTimeForType`. * + * Only use this API if you have an advanced use case that's not solved by the + * {@link SavedObjectsRepository.createPointInTimeFinder} method. + * * @remarks * While the `keepAlive` that is provided will cause a PIT to automatically close, * it is highly recommended to explicitly close a PIT when you are done with it @@ -1893,9 +1970,66 @@ export class SavedObjectsRepository { const { body } = await this.client.closePointInTime({ body: { id }, }); + return body; } + /** + * Returns a {@link ISavedObjectsPointInTimeFinder} to help page through + * large sets of saved objects. We strongly recommend using this API for + * any `find` queries that might return more than 1000 saved objects, + * however this API is only intended for use in server-side "batch" + * processing of objects where you are collecting all objects in memory + * or streaming them back to the client. + * + * Do NOT use this API in a route handler to facilitate paging through + * saved objects on the client-side unless you are streaming all of the + * results back to the client at once. Because the returned generator is + * stateful, you cannot rely on subsequent http requests retrieving new + * pages from the same Kibana server in multi-instance deployments. + * + * This generator wraps calls to {@link SavedObjectsRepository.find} and + * iterates over multiple pages of results using `_pit` and `search_after`. + * This will open a new Point-In-Time (PIT), and continue paging until a + * set of results is received that's smaller than the designated `perPage`. + * + * Once you have retrieved all of the results you need, it is recommended + * to call `close()` to clean up the PIT and prevent Elasticsearch from + * consuming resources unnecessarily. This is only required if you are + * done iterating and have not yet paged through all of the results: the + * PIT will automatically be closed for you once you reach the last page + * of results, or if the underlying call to `find` fails for any reason. + * + * @example + * ```ts + * const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { + * type: 'visualization', + * search: 'foo*', + * perPage: 100, + * }; + * + * const finder = savedObjectsClient.createPointInTimeFinder(findOptions); + * + * const responses: SavedObjectFindResponse[] = []; + * for await (const response of finder.find()) { + * responses.push(...response); + * if (doneSearching) { + * await finder.close(); + * } + * } + * ``` + */ + createPointInTimeFinder( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ): ISavedObjectsPointInTimeFinder { + return new PointInTimeFinder(findOptions, { + logger: this._logger, + client: this, + ...dependencies, + }); + } + /** * Returns index specified by the given type or the default index * @@ -1979,7 +2113,7 @@ export class SavedObjectsRepository { throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); } - const { body, statusCode } = await this.client.get>( + const { body, statusCode } = await this.client.get( { id: this._serializer.generateRawId(undefined, type, id), index: this.getIndexForType(type), @@ -1990,8 +2124,7 @@ export class SavedObjectsRepository { ); const indexFound = statusCode !== 404; - const docFound = indexFound && body.found === true; - if (docFound) { + if (indexFound && isFoundGetResponse(body)) { if (!this.rawDocExistsInNamespace(body, namespace)) { throw SavedObjectsErrorHelpers.createConflictError(type, id); } @@ -2016,7 +2149,7 @@ export class SavedObjectsRepository { } const rawId = this._serializer.generateRawId(undefined, type, id); - const { body, statusCode } = await this.client.get>( + const { body, statusCode } = await this.client.get( { id: rawId, index: this.getIndexForType(type), @@ -2025,17 +2158,20 @@ export class SavedObjectsRepository { ); const indexFound = statusCode !== 404; - const docFound = indexFound && body.found === true; - if (!docFound || !this.rawDocExistsInNamespace(body, namespace)) { + if ( + !indexFound || + !isFoundGetResponse(body) || + !this.rawDocExistsInNamespace(body, namespace) + ) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return body as SavedObjectsRawDoc; + return body; } private getSavedObjectFromSource( type: string, id: string, - doc: { _seq_no: number; _primary_term: number; _source: SavedObjectsRawDocSource } + doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } ): SavedObject { const { originId, updated_at: updatedAt } = doc._source; @@ -2145,3 +2281,15 @@ const normalizeNamespace = (namespace?: string) => { const errorContent = (error: DecoratedError) => error.output.payload; const unique = (array: string[]) => [...new Set(array)]; + +/** + * Type and type guard function for converting a possibly not existant doc to an existant doc. + */ +type GetResponseFound = estypes.GetResponse & + Required< + Pick, '_primary_term' | '_seq_no' | '_version' | '_source'> + >; + +const isFoundGetResponse = ( + doc: estypes.GetResponse +): doc is GetResponseFound => doc.found; diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index 26aa152c630ad..9d9a2eb14b495 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -10,12 +10,14 @@ import { SavedObjectsRepository } from './repository'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; import { KibanaMigrator } from '../../migrations'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { loggerMock, MockedLogger } from '../../../logging/logger.mock'; jest.mock('./repository'); const { SavedObjectsRepository: originalRepository } = jest.requireActual('./repository'); describe('SavedObjectsRepository#createRepository', () => { + let logger: MockedLogger; const callAdminCluster = jest.fn(); const typeRegistry = new SavedObjectTypeRegistry(); @@ -59,6 +61,7 @@ describe('SavedObjectsRepository#createRepository', () => { const RepositoryConstructor = (SavedObjectsRepository as unknown) as jest.Mock; beforeEach(() => { + logger = loggerMock.create(); RepositoryConstructor.mockClear(); }); @@ -69,6 +72,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry, '.kibana-test', callAdminCluster, + logger, ['unMappedType1', 'unmappedType2'] ); } catch (e) { @@ -84,6 +88,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry, '.kibana-test', callAdminCluster, + logger, [], SavedObjectsRepository ); @@ -102,6 +107,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry, '.kibana-test', callAdminCluster, + logger, ['hiddenType', 'hiddenType', 'hiddenType'], SavedObjectsRepository ); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index 267d671361184..b15560b82ab31 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -86,7 +86,7 @@ describe('getSearchDsl', () => { const opts = { type: 'foo', sortField: 'bar', - sortOrder: 'baz', + sortOrder: 'asc' as const, pit: { id: 'abc123' }, }; @@ -109,10 +109,10 @@ describe('getSearchDsl', () => { it('returns searchAfter if provided', () => { getQueryParams.mockReturnValue({ a: 'a' }); getSortingParams.mockReturnValue({ b: 'b' }); - expect(getSearchDsl(mappings, registry, { type: 'foo', searchAfter: [1, 'bar'] })).toEqual({ + expect(getSearchDsl(mappings, registry, { type: 'foo', searchAfter: ['1', 'bar'] })).toEqual({ a: 'a', b: 'b', - search_after: [1, 'bar'], + search_after: ['1', 'bar'], }); }); @@ -123,14 +123,14 @@ describe('getSearchDsl', () => { expect( getSearchDsl(mappings, registry, { type: 'foo', - searchAfter: [1, 'bar'], + searchAfter: ['1', 'bar'], pit: { id: 'abc123' }, }) ).toEqual({ a: 'a', b: 'b', pit: { id: 'abc123' }, - search_after: [1, 'bar'], + search_after: ['1', 'bar'], }); }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 9820544f02bd1..64b3dd428fb8b 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -8,6 +8,7 @@ import Boom from '@hapi/boom'; +import type { estypes } from '@elastic/elasticsearch'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsPitParams } from '../../../types'; import { getQueryParams, HasReferenceQueryParams, SearchOperator } from './query_params'; @@ -23,9 +24,9 @@ interface GetSearchDslOptions { defaultSearchOperator?: SearchOperator; searchFields?: string[]; rootSearchFields?: string[]; - searchAfter?: unknown[]; + searchAfter?: estypes.Id[]; sortField?: string; - sortOrder?: string; + sortOrder?: estypes.SortOrder; namespaces?: string[]; pit?: SavedObjectsPitParams; typeToNamespacesMap?: Map; @@ -80,6 +81,6 @@ export function getSearchDsl( }), ...getSortingParams(mappings, type, sortField, sortOrder), ...(pit ? getPitParams(pit) : {}), - ...(searchAfter ? { search_after: searchAfter } : {}), + search_after: searchAfter, }; } diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts index e3bfba6a80f59..64849c308f3f0 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import { getProperty, IndexMapping } from '../../../mappings'; @@ -15,8 +16,8 @@ export function getSortingParams( mappings: IndexMapping, type: string | string[], sortField?: string, - sortOrder?: string -) { + sortOrder?: estypes.SortOrder +): { sort?: estypes.SortContainer[] } { if (!sortField) { return {}; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index ecca652cace37..544e92e32f1a1 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -8,9 +8,10 @@ import { SavedObjectsClientContract } from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; +import { savedObjectsPointInTimeFinderMock } from './lib/point_in_time_finder.mock'; -const create = () => - (({ +const create = () => { + const mock = ({ errors: SavedObjectsErrorHelpers, create: jest.fn(), bulkCreate: jest.fn(), @@ -21,12 +22,20 @@ const create = () => find: jest.fn(), get: jest.fn(), closePointInTime: jest.fn(), + createPointInTimeFinder: jest.fn(), openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), addToNamespaces: jest.fn(), deleteFromNamespaces: jest.fn(), removeReferencesTo: jest.fn(), - } as unknown) as jest.Mocked); + } as unknown) as jest.Mocked; + + mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ + savedObjectsMock: mock, + }); + + return mock; +}; export const savedObjectsClientMock = { create }; diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 7cbddaf195dc9..29381c7e418b5 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -54,6 +54,45 @@ test(`#bulkCreate`, async () => { expect(result).toBe(returnValue); }); +describe(`#createPointInTimeFinder`, () => { + test(`calls repository with options and default dependencies`, () => { + const returnValue = Symbol(); + const mockRepository = { + createPointInTimeFinder: jest.fn().mockReturnValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const options = Symbol(); + const result = client.createPointInTimeFinder(options); + + expect(mockRepository.createPointInTimeFinder).toHaveBeenCalledWith(options, { + client, + }); + expect(result).toBe(returnValue); + }); + + test(`calls repository with options and custom dependencies`, () => { + const returnValue = Symbol(); + const mockRepository = { + createPointInTimeFinder: jest.fn().mockReturnValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const options = Symbol(); + const dependencies = { + client: { + find: Symbol(), + openPointInTimeForType: Symbol(), + closePointInTime: Symbol(), + }, + }; + const result = client.createPointInTimeFinder(options, dependencies); + + expect(mockRepository.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); + expect(result).toBe(returnValue); + }); +}); + test(`#delete`, async () => { const returnValue = Symbol(); const mockRepository = { diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index b078f3eef018c..9a0ccb88d3555 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { ISavedObjectsRepository } from './lib'; +import type { + ISavedObjectsRepository, + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, + SavedObjectsCreatePointInTimeFinderDependencies, +} from './lib'; import { SavedObject, SavedObjectError, @@ -157,7 +162,7 @@ export interface SavedObjectsFindResult extends SavedObject { * await savedObjectsClient.closePointInTime(page2.pit_id); * ``` */ - sort?: unknown[]; + sort?: string[]; } /** @@ -587,6 +592,9 @@ export class SavedObjectsClient { * Opens a Point In Time (PIT) against the indices for the specified Saved Object types. * The returned `id` can then be passed to {@link SavedObjectsClient.find} to search * against that PIT. + * + * Only use this API if you have an advanced use case that's not solved by the + * {@link SavedObjectsClient.createPointInTimeFinder} method. */ async openPointInTimeForType( type: string | string[], @@ -599,8 +607,67 @@ export class SavedObjectsClient { * Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the * Elasticsearch client, and is included in the Saved Objects Client as a convenience * for consumers who are using {@link SavedObjectsClient.openPointInTimeForType}. + * + * Only use this API if you have an advanced use case that's not solved by the + * {@link SavedObjectsClient.createPointInTimeFinder} method. */ async closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions) { return await this._repository.closePointInTime(id, options); } + + /** + * Returns a {@link ISavedObjectsPointInTimeFinder} to help page through + * large sets of saved objects. We strongly recommend using this API for + * any `find` queries that might return more than 1000 saved objects, + * however this API is only intended for use in server-side "batch" + * processing of objects where you are collecting all objects in memory + * or streaming them back to the client. + * + * Do NOT use this API in a route handler to facilitate paging through + * saved objects on the client-side unless you are streaming all of the + * results back to the client at once. Because the returned generator is + * stateful, you cannot rely on subsequent http requests retrieving new + * pages from the same Kibana server in multi-instance deployments. + * + * The generator wraps calls to {@link SavedObjectsClient.find} and iterates + * over multiple pages of results using `_pit` and `search_after`. This will + * open a new Point-In-Time (PIT), and continue paging until a set of + * results is received that's smaller than the designated `perPage`. + * + * Once you have retrieved all of the results you need, it is recommended + * to call `close()` to clean up the PIT and prevent Elasticsearch from + * consuming resources unnecessarily. This is only required if you are + * done iterating and have not yet paged through all of the results: the + * PIT will automatically be closed for you once you reach the last page + * of results, or if the underlying call to `find` fails for any reason. + * + * @example + * ```ts + * const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { + * type: 'visualization', + * search: 'foo*', + * perPage: 100, + * }; + * + * const finder = savedObjectsClient.createPointInTimeFinder(findOptions); + * + * const responses: SavedObjectFindResponse[] = []; + * for await (const response of finder.find()) { + * responses.push(...response); + * if (doneSearching) { + * await finder.close(); + * } + * } + * ``` + */ + createPointInTimeFinder( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ): ISavedObjectsPointInTimeFinder { + return this._repository.createPointInTimeFinder(findOptions, { + client: this, + // Include dependencies last so that SO client wrappers have their settings applied. + ...dependencies, + }); + } } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 11a694c72f29f..ecda120e025d8 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import { SavedObjectsClient } from './service/saved_objects_client'; import { SavedObjectsTypeMappingDefinition } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; @@ -79,7 +80,7 @@ export interface SavedObjectsFindOptions { page?: number; perPage?: number; sortField?: string; - sortOrder?: string; + sortOrder?: estypes.SortOrder; /** * An array of fields to include in the results * @example @@ -93,7 +94,7 @@ export interface SavedObjectsFindOptions { /** * Use the sort values from the previous page to retrieve the next page of results. */ - searchAfter?: unknown[]; + searchAfter?: estypes.Id[]; /** * The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not * be modified. If used in conjunction with `searchFields`, both are concatenated together. diff --git a/src/core/server/saved_objects/version/encode_hit_version.ts b/src/core/server/saved_objects/version/encode_hit_version.ts index 614666c6e1da6..979df93dc57b5 100644 --- a/src/core/server/saved_objects/version/encode_hit_version.ts +++ b/src/core/server/saved_objects/version/encode_hit_version.ts @@ -12,6 +12,6 @@ import { encodeVersion } from './encode_version'; * Helper for encoding a version from a "hit" (hits.hits[#] from _search) or * "doc" (body from GET, update, etc) object */ -export function encodeHitVersion(response: { _seq_no: number; _primary_term: number }) { +export function encodeHitVersion(response: { _seq_no?: number; _primary_term?: number }) { return encodeVersion(response._seq_no, response._primary_term); } diff --git a/src/core/server/saved_objects/version/encode_version.ts b/src/core/server/saved_objects/version/encode_version.ts index fa778ee931e41..9c0b0a7428f38 100644 --- a/src/core/server/saved_objects/version/encode_version.ts +++ b/src/core/server/saved_objects/version/encode_version.ts @@ -13,7 +13,7 @@ import { encodeBase64 } from './base64'; * that can be used in the saved object API in place of numeric * version numbers */ -export function encodeVersion(seqNo: number, primaryTerm: number) { +export function encodeVersion(seqNo?: number, primaryTerm?: number) { if (!Number.isInteger(primaryTerm)) { throw new TypeError('_primary_term from elasticsearch must be an integer'); } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 580315973ce8f..de96c5ccfb81e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -4,6 +4,7 @@ ```ts +import { AddConfigDeprecation } from '@kbn/config'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from '@hapi/boom'; import { BulkIndexDocumentsParams } from 'elasticsearch'; @@ -35,7 +36,6 @@ import { ClusterStateParams } from 'elasticsearch'; import { ClusterStatsParams } from 'elasticsearch'; import { ConfigDeprecation } from '@kbn/config'; import { ConfigDeprecationFactory } from '@kbn/config'; -import { ConfigDeprecationLogger } from '@kbn/config'; import { ConfigDeprecationProvider } from '@kbn/config'; import { ConfigOptions } from 'elasticsearch'; import { ConfigPath } from '@kbn/config'; @@ -50,6 +50,7 @@ import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { Duration as Duration_2 } from 'moment-timezone'; import { EnvironmentMode } from '@kbn/config'; +import { estypes } from '@elastic/elasticsearch'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; @@ -168,6 +169,8 @@ import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { URL } from 'url'; +export { AddConfigDeprecation } + // @public export interface AppCategory { ariaLabel?: string; @@ -296,7 +299,7 @@ export class BasePath { // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts // // @internal (undocumented) -export function bootstrap({ configs, cliArgs, applyConfigOverrides, features, }: BootstrapArgs): Promise; +export function bootstrap({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs): Promise; // @public export interface Capabilities { @@ -342,7 +345,7 @@ export const config: { pingTimeout: Type; logQueries: Type; ssl: import("@kbn/config-schema").ObjectType<{ - verificationMode: Type<"none" | "certificate" | "full">; + verificationMode: Type<"certificate" | "none" | "full">; certificateAuthorities: Type; certificate: Type; key: Type; @@ -373,8 +376,6 @@ export { ConfigDeprecation } export { ConfigDeprecationFactory } -export { ConfigDeprecationLogger } - export { ConfigDeprecationProvider } export { ConfigPath } @@ -490,6 +491,8 @@ export interface CoreSetup; @@ -829,12 +832,40 @@ export interface DeprecationInfo { url: string; } +// Warning: (ae-missing-release-tag) "DeprecationsDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface DeprecationsDetails { + // (undocumented) + correctiveActions: { + api?: { + path: string; + method: 'POST' | 'PUT'; + body?: { + [key: string]: any; + }; + }; + manualSteps?: string[]; + }; + // (undocumented) + documentationUrl?: string; + level: 'warning' | 'critical' | 'fetch_error'; + // (undocumented) + message: string; +} + // @public export interface DeprecationSettings { docLinksKey: string; message: string; } +// @public +export interface DeprecationsServiceSetup { + // (undocumented) + registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; +} + // @public export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; @@ -938,6 +969,16 @@ export type GetAuthState = (request: KibanaRequest | LegacyRequest) state: T; }; +// Warning: (ae-missing-release-tag) "GetDeprecationsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface GetDeprecationsContext { + // (undocumented) + esClient: IScopedClusterClient; + // (undocumented) + savedObjectsClient: SavedObjectsClientContract; +} + // @public (undocumented) export interface GetResponse { // (undocumented) @@ -1177,6 +1218,12 @@ export type ISavedObjectsExporter = PublicMethodsOf; // @public (undocumented) export type ISavedObjectsImporter = PublicMethodsOf; +// @public (undocumented) +export interface ISavedObjectsPointInTimeFinder { + close: () => Promise; + find: () => AsyncGenerator; +} + // @public export type ISavedObjectsRepository = Pick; @@ -1258,10 +1305,10 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory; // @public export const kibanaResponseFactory: { - custom: | Buffer | Error | Stream | { + custom: | Error | Buffer | { message: string | Error; attributes?: Record | undefined; - } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; @@ -1905,6 +1952,16 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; }; +// Warning: (ae-missing-release-tag) "RegisterDeprecationsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface RegisterDeprecationsConfig { + // Warning: (ae-forgotten-export) The symbol "MaybePromise" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getDeprecations: (context: GetDeprecationsContext) => MaybePromise; +} + // @public export type RequestHandler

= (context: Context, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; @@ -2219,6 +2276,7 @@ export class SavedObjectsClient { checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) @@ -2321,6 +2379,15 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { version?: string; } +// @public (undocumented) +export interface SavedObjectsCreatePointInTimeFinderDependencies { + // (undocumented) + client: Pick; +} + +// @public (undocumented) +export type SavedObjectsCreatePointInTimeFinderOptions = Omit; + // @public (undocumented) export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { refresh?: boolean; @@ -2491,12 +2558,12 @@ export interface SavedObjectsFindOptions { preference?: string; rootSearchFields?: string[]; search?: string; - searchAfter?: unknown[]; + searchAfter?: estypes.Id[]; searchFields?: string[]; // (undocumented) sortField?: string; // (undocumented) - sortOrder?: string; + sortOrder?: estypes.SortOrder; // (undocumented) type: string | string[]; typeToNamespacesMap?: Map; @@ -2527,7 +2594,7 @@ export interface SavedObjectsFindResponse { // @public (undocumented) export interface SavedObjectsFindResult extends SavedObject { score: number; - sort?: unknown[]; + sort?: string[]; } // @public @@ -2718,11 +2785,12 @@ export interface SavedObjectsIncrementCounterField { } // @public (undocumented) -export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { initialize?: boolean; // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; refresh?: MutatingOperationRefreshSetting; + upsertAttributes?: Attributes; } // @public @@ -2811,17 +2879,18 @@ export class SavedObjectsRepository { checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; + incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index f2a2b10fdbfde..fcf09b0295bcb 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -205,19 +205,3 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockLoggingService.setup).not.toHaveBeenCalled(); expect(mockI18nService.setup).not.toHaveBeenCalled(); }); - -test(`doesn't validate config if env.isDevCliParent is true`, async () => { - const devParentEnv = Env.createDefault(REPO_ROOT, { - ...getEnvOptions(), - isDevCliParent: true, - }); - - const server = new Server(rawConfigService, devParentEnv, logger); - await server.setup(); - - expect(mockEnsureValidConfiguration).not.toHaveBeenCalled(); - expect(mockContextService.setup).toHaveBeenCalled(); - expect(mockHttpService.setup).toHaveBeenCalled(); - expect(mockElasticsearchService.setup).toHaveBeenCalled(); - expect(mockSavedObjectsService.setup).toHaveBeenCalled(); -}); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index ef5164a8c48e1..b575b2779082c 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -41,6 +41,7 @@ import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; +import { DeprecationsService } from './deprecations'; import { CoreRouteHandlerContext } from './core_route_handler_context'; import { config as externalUrlConfig } from './external_url'; @@ -67,6 +68,7 @@ export class Server { private readonly coreApp: CoreApp; private readonly coreUsageData: CoreUsageDataService; private readonly i18n: I18nService; + private readonly deprecations: DeprecationsService; private readonly savedObjectsStartPromise: Promise; private resolveSavedObjectsStartPromise?: (value: SavedObjectsServiceStart) => void; @@ -102,6 +104,7 @@ export class Server { this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); this.i18n = new I18nService(core); + this.deprecations = new DeprecationsService(core); this.savedObjectsStartPromise = new Promise((resolve) => { this.resolveSavedObjectsStartPromise = resolve; @@ -120,13 +123,10 @@ export class Server { }); const legacyConfigSetup = await this.legacy.setupLegacyConfig(); - // rely on dev server to validate config, don't validate in the parent process - if (!this.env.isDevCliParent) { - // Immediately terminate in case of invalid configuration - // This needs to be done after plugin discovery - await this.configService.validate(); - await ensureValidConfiguration(this.configService, legacyConfigSetup); - } + // Immediately terminate in case of invalid configuration + // This needs to be done after plugin discovery + await this.configService.validate(); + await ensureValidConfiguration(this.configService, legacyConfigSetup); const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: @@ -195,6 +195,12 @@ export class Server { loggingSystem: this.loggingSystem, }); + const deprecationsSetup = this.deprecations.setup({ + http: httpSetup, + elasticsearch: elasticsearchServiceSetup, + coreUsageData: coreUsageDataSetup, + }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -209,6 +215,7 @@ export class Server { httpResources: httpResourcesSetup, logging: loggingSetup, metrics: metricsSetup, + deprecations: deprecationsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -288,6 +295,7 @@ export class Server { await this.metrics.stop(); await this.status.stop(); await this.logging.stop(); + this.deprecations.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 6bd805d55af1d..ab1d6c6d95d0a 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -37,6 +37,7 @@ export type { SavedObjectsClientContract, SavedObjectsNamespaceType, } 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'; diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts index e2dc2c7d99a93..b0776c48f3bed 100644 --- a/src/core/server/utils/index.ts +++ b/src/core/server/utils/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export * from './crypto'; export * from './from_root'; export * from './package_json'; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 5e274712ad3a7..d702fed73778f 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -70,7 +70,6 @@ export function createRootWithSettings( dist: false, ...cliArgs, }, - isDevCliParent: false, }); return new Root( diff --git a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index c67cd325572ff..96725d4405112 100644 --- a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -29,9 +29,6 @@ export function convertPanelStateToSavedDashboardPanel( panelState: DashboardPanelState, version: string ): SavedDashboardPanel { - const customTitle: string | undefined = panelState.explicitInput.title - ? (panelState.explicitInput.title as string) - : undefined; const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId; return { version, @@ -39,7 +36,7 @@ export function convertPanelStateToSavedDashboardPanel( gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), - ...(customTitle && { title: customTitle }), + ...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; } diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index a50aadc12e6c0..9e3018fb512c3 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -597,7 +597,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` restrictWidth="500px" >

@@ -232,12 +232,12 @@ exports[`after fetch hideWriteControls 1`] = ` restrictWidth={true} >
@@ -379,12 +379,12 @@ exports[`after fetch initialFilter 1`] = ` restrictWidth={true} >
@@ -525,12 +525,12 @@ exports[`after fetch renders all table rows 1`] = ` restrictWidth={true} >
@@ -671,12 +671,12 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` restrictWidth={true} >
@@ -817,12 +817,12 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` restrictWidth={true} >
diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 145aaa64fa3ad..60e74a3fa126c 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -21,7 +21,6 @@ It is wired into the `TopNavMenu` component, but can be used independently. ### Fetch Query Suggestions The `getQuerySuggestions` function helps to construct a query. -KQL suggestion functions are registered in X-Pack, so this API does not return results in OSS. ```.ts diff --git a/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts index 37a28fea53342..1c50f0704910a 100644 --- a/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { IndexPatternsContract } from '../index_patterns'; import { IndexPatternSpec } from '..'; +import { SavedObjectReference } from '../../../../../core/types'; const name = 'indexPatternLoad'; const type = 'index_pattern'; @@ -57,4 +58,29 @@ export const getIndexPatternLoadMeta = (): Omit< }), }, }, + extract(state) { + const refName = 'indexPatternLoad.id'; + const references: SavedObjectReference[] = [ + { + name: refName, + type: 'search', + id: state.id[0] as string, + }, + ]; + return { + state: { + ...state, + id: [refName], + }, + references, + }; + }, + + inject(state, references) { + const reference = references.find((ref) => ref.name === 'indexPatternLoad.id'); + if (reference) { + state.id[0] = reference.id; + } + return state; + }, }); diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index dc1de8d1338f1..12dc0c1b2599d 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -5,19 +5,18 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; -import { SearchResponse } from 'elasticsearch'; -import { Search } from '@elastic/elasticsearch/api/requestParams'; import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; export const ES_SEARCH_STRATEGY = 'es'; -export type ISearchRequestParams> = { +export type ISearchRequestParams = { trackTotalHits?: boolean; -} & Search; +} & estypes.SearchRequest; export interface IEsSearchRequest extends IKibanaSearchRequest { indexType?: string; } -export type IEsSearchResponse = IKibanaSearchResponse>; +export type IEsSearchResponse = IKibanaSearchResponse>; diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 98d7a2c45b4fc..22a7150d4a64e 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -15,6 +15,14 @@ import { Query, uniqFilters } from '../../query'; import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type'; import { KibanaQueryOutput } from './kibana_context_type'; import { KibanaTimerangeOutput } from './timerange'; +import { SavedObjectReference } from '../../../../../core/types'; +import { SavedObjectsClientCommon } from '../../index_patterns'; +import { Filter } from '../../es_query/filters'; + +/** @internal */ +export interface KibanaContextStartDependencies { + savedObjectsClient: SavedObjectsClientCommon; +} interface Arguments { q?: KibanaQueryOutput | null; @@ -40,75 +48,108 @@ const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) => (n: any) => JSON.stringify(n.query) ); -export const kibanaContextFunction: ExpressionFunctionKibanaContext = { - name: 'kibana_context', - type: 'kibana_context', - inputTypes: ['kibana_context', 'null'], - help: i18n.translate('data.search.functions.kibana_context.help', { - defaultMessage: 'Updates kibana global context', - }), - args: { - q: { - types: ['kibana_query', 'null'], - aliases: ['query', '_'], - default: null, - help: i18n.translate('data.search.functions.kibana_context.q.help', { - defaultMessage: 'Specify Kibana free form text query', - }), - }, - filters: { - types: ['kibana_filter', 'null'], - multi: true, - help: i18n.translate('data.search.functions.kibana_context.filters.help', { - defaultMessage: 'Specify Kibana generic filters', - }), +export const getKibanaContextFn = ( + getStartDependencies: ( + getKibanaRequest: ExecutionContext['getKibanaRequest'] + ) => Promise +) => { + const kibanaContextFunction: ExpressionFunctionKibanaContext = { + name: 'kibana_context', + type: 'kibana_context', + inputTypes: ['kibana_context', 'null'], + help: i18n.translate('data.search.functions.kibana_context.help', { + defaultMessage: 'Updates kibana global context', + }), + args: { + q: { + types: ['kibana_query', 'null'], + aliases: ['query', '_'], + default: null, + help: i18n.translate('data.search.functions.kibana_context.q.help', { + defaultMessage: 'Specify Kibana free form text query', + }), + }, + filters: { + types: ['kibana_filter', 'null'], + multi: true, + help: i18n.translate('data.search.functions.kibana_context.filters.help', { + defaultMessage: 'Specify Kibana generic filters', + }), + }, + timeRange: { + types: ['timerange', 'null'], + default: null, + help: i18n.translate('data.search.functions.kibana_context.timeRange.help', { + defaultMessage: 'Specify Kibana time range filter', + }), + }, + savedSearchId: { + types: ['string', 'null'], + default: null, + help: i18n.translate('data.search.functions.kibana_context.savedSearchId.help', { + defaultMessage: 'Specify saved search ID to be used for queries and filters', + }), + }, }, - timeRange: { - types: ['timerange', 'null'], - default: null, - help: i18n.translate('data.search.functions.kibana_context.timeRange.help', { - defaultMessage: 'Specify Kibana time range filter', - }), + + extract(state) { + const references: SavedObjectReference[] = []; + if (state.savedSearchId.length && typeof state.savedSearchId[0] === 'string') { + const refName = 'kibana_context.savedSearchId'; + references.push({ + name: refName, + type: 'search', + id: state.savedSearchId[0] as string, + }); + return { + state: { + ...state, + savedSearchId: [refName], + }, + references, + }; + } + return { state, references }; }, - savedSearchId: { - types: ['string', 'null'], - default: null, - help: i18n.translate('data.search.functions.kibana_context.savedSearchId.help', { - defaultMessage: 'Specify saved search ID to be used for queries and filters', - }), + + inject(state, references) { + const reference = references.find((r) => r.name === 'kibana_context.savedSearchId'); + if (reference) { + state.savedSearchId[0] = reference.id; + } + return state; }, - }, - async fn(input, args, { getSavedObject }) { - const timeRange = args.timeRange || input?.timeRange; - let queries = mergeQueries(input?.query, args?.q || []); - let filters = [...(input?.filters || []), ...(args?.filters?.map(unboxExpressionValue) || [])]; + async fn(input, args, { getKibanaRequest }) { + const { savedObjectsClient } = await getStartDependencies(getKibanaRequest); - if (args.savedSearchId) { - if (typeof getSavedObject !== 'function') { - throw new Error( - '"getSavedObject" function not available in execution context. ' + - 'When you execute expression you need to add extra execution context ' + - 'as the third argument and provide "getSavedObject" implementation.' - ); - } - const obj = await getSavedObject('search', args.savedSearchId); - const search = obj.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; - const { query, filter } = getParsedValue(search.searchSourceJSON, {}); + const timeRange = args.timeRange || input?.timeRange; + let queries = mergeQueries(input?.query, args?.q || []); + let filters = [ + ...(input?.filters || []), + ...((args?.filters?.map(unboxExpressionValue) || []) as Filter[]), + ]; - if (query) { - queries = mergeQueries(queries, query); - } - if (filter) { - filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])]; + if (args.savedSearchId) { + const obj = await savedObjectsClient.get('search', args.savedSearchId); + const search = (obj.attributes as any).kibanaSavedObjectMeta.searchSourceJSON as string; + const { query, filter } = getParsedValue(search, {}); + + if (query) { + queries = mergeQueries(queries, query); + } + if (filter) { + filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])]; + } } - } - return { - type: 'kibana_context', - query: queries, - filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled), - timeRange, - }; - }, + return { + type: 'kibana_context', + query: queries, + filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled), + timeRange, + }; + }, + }; + return kibanaContextFunction; }; diff --git a/src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts b/src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts index 6013b3d6c6f5f..99acbce8935c4 100644 --- a/src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts +++ b/src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts @@ -14,7 +14,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ISearchSource } from 'src/plugins/data/public'; import { RequestStatistics } from 'src/plugins/inspector/common'; @@ -50,7 +50,7 @@ export function getRequestInspectorStats(searchSource: ISearchSource) { /** @public */ export function getResponseInspectorStats( - resp: SearchResponse, + resp: estypes.SearchResponse, searchSource?: ISearchSource ) { const lastRequest = diff --git a/src/plugins/data/common/search/search_source/fetch/request_error.ts b/src/plugins/data/common/search/search_source/fetch/request_error.ts index 14185d7d5afd3..d8c750d011b03 100644 --- a/src/plugins/data/common/search/search_source/fetch/request_error.ts +++ b/src/plugins/data/common/search/search_source/fetch/request_error.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { KbnError } from '../../../../../kibana_utils/common'; import { SearchError } from './types'; @@ -16,8 +16,8 @@ import { SearchError } from './types'; * @param {Object} resp - optional HTTP response */ export class RequestFailure extends KbnError { - public resp?: SearchResponse; - constructor(err: SearchError | null = null, resp?: SearchResponse) { + public resp?: estypes.SearchResponse; + constructor(err: SearchError | null = null, resp?: estypes.SearchResponse) { super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`); this.resp = resp; diff --git a/src/plugins/data/common/search/search_source/fetch/types.ts b/src/plugins/data/common/search/search_source/fetch/types.ts index 2387d9dbffa3a..8e8a9f1025b80 100644 --- a/src/plugins/data/common/search/search_source/fetch/types.ts +++ b/src/plugins/data/common/search/search_source/fetch/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { LegacyFetchHandlers } from '../legacy/types'; import { GetConfigFn } from '../../../types'; @@ -25,7 +25,10 @@ export interface FetchHandlers { * Callback which can be used to hook into responses, modify them, or perform * side effects like displaying UI errors on the client. */ - onResponse: (request: SearchRequest, response: SearchResponse) => SearchResponse; + onResponse: ( + request: SearchRequest, + response: estypes.SearchResponse + ) => estypes.SearchResponse; /** * These handlers are only used by the legacy defaultSearchStrategy and can be removed * once that strategy has been deprecated. diff --git a/src/plugins/data/common/search/search_source/legacy/call_client.ts b/src/plugins/data/common/search/search_source/legacy/call_client.ts index a288cdc22c576..4c1156aac7015 100644 --- a/src/plugins/data/common/search/search_source/legacy/call_client.ts +++ b/src/plugins/data/common/search/search_source/legacy/call_client.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { FetchHandlers, SearchRequest } from '../fetch'; import { defaultSearchStrategy } from './default_search_strategy'; import { ISearchOptions } from '../../index'; @@ -21,7 +21,7 @@ export function callClient( [SearchRequest, ISearchOptions] > = searchRequests.map((request, i) => [request, requestsOptions[i]]); const requestOptionsMap = new Map(requestOptionEntries); - const requestResponseMap = new Map>>(); + const requestResponseMap = new Map>>(); const { searching, abort } = defaultSearchStrategy.search({ searchRequests, diff --git a/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts b/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts index e42ef6617594a..ff8ae2d19bd56 100644 --- a/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts +++ b/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { UI_SETTINGS } from '../../../constants'; import { FetchHandlers, SearchRequest } from '../fetch'; import { ISearchOptions } from '../../index'; @@ -57,9 +57,11 @@ async function delayedFetch( options: ISearchOptions, fetchHandlers: FetchHandlers, ms: number -): Promise> { +): Promise> { if (ms === 0) { - return callClient([request], [options], fetchHandlers)[0]; + return callClient([request], [options], fetchHandlers)[0] as Promise< + estypes.SearchResponse + >; } const i = requestsToFetch.length; diff --git a/src/plugins/data/common/search/search_source/legacy/types.ts b/src/plugins/data/common/search/search_source/legacy/types.ts index 5a60d1082b0ed..a4328528fd662 100644 --- a/src/plugins/data/common/search/search_source/legacy/types.ts +++ b/src/plugins/data/common/search/search_source/legacy/types.ts @@ -7,8 +7,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { ApiResponse } from '@elastic/elasticsearch'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes, ApiResponse } from '@elastic/elasticsearch'; import { FetchHandlers, SearchRequest } from '../fetch'; interface MsearchHeaders { @@ -28,7 +27,7 @@ export interface MsearchRequestBody { // @internal export interface MsearchResponse { - body: ApiResponse<{ responses: Array> }>; + body: ApiResponse<{ responses: Array> }>; } // @internal @@ -51,6 +50,6 @@ export interface SearchStrategyProvider { } export interface SearchStrategyResponse { - searching: Promise>>; + searching: Promise>>; abort: () => void; } diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index f63d2dfec142c..a7ba8ab9576b6 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -31,6 +31,13 @@ export interface SearchSessionSavedObjectAttributes { * Expiration time of the session. Expiration itself is managed by Elasticsearch. */ expires: string; + /** + * Time of transition into completed state, + * + * Can be "null" in case already completed session + * transitioned into in-progress session + */ + completed?: string | null; /** * status */ diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index 6b288c4507f06..eb9d859664c4d 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -18,6 +18,11 @@ import { import { ConfigSchema } from '../../config'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { createUsageCollector } from './collectors'; +import { + KUERY_LANGUAGE_NAME, + setupKqlQuerySuggestionProvider, +} from './providers/kql_query_suggestion'; +import { DataPublicPluginStart, DataStartDependencies } from '../types'; export class AutocompleteService { autocompleteConfig: ConfigSchema['autocomplete']; @@ -31,12 +36,6 @@ export class AutocompleteService { private readonly querySuggestionProviders: Map = new Map(); private getValueSuggestions?: ValueSuggestionsGetFn; - private addQuerySuggestionProvider = (language: string, provider: QuerySuggestionGetFn): void => { - if (language && provider && this.autocompleteConfig.querySuggestions.enabled) { - this.querySuggestionProviders.set(language, provider); - } - }; - private getQuerySuggestions: QuerySuggestionGetFn = (args) => { const { language } = args; const provider = this.querySuggestionProviders.get(language); @@ -50,7 +49,7 @@ export class AutocompleteService { /** @public **/ public setup( - core: CoreSetup, + core: CoreSetup, { timefilter, usageCollection, @@ -62,11 +61,15 @@ export class AutocompleteService { ? setupValueSuggestionProvider(core, { timefilter, usageCollector }) : getEmptyValueSuggestions; - return { - addQuerySuggestionProvider: this.addQuerySuggestionProvider, + if (this.autocompleteConfig.querySuggestions.enabled) { + this.querySuggestionProviders.set(KUERY_LANGUAGE_NAME, setupKqlQuerySuggestionProvider(core)); + } - /** @obsolete **/ - /** please use "getProvider" only from the start contract **/ + return { + /** + * @deprecated + * please use "getQuerySuggestions" from the start contract + */ getQuerySuggestions: this.getQuerySuggestions, }; } diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/README.md b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/README.md new file mode 100644 index 0000000000000..2ab87a7a490c1 --- /dev/null +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/README.md @@ -0,0 +1 @@ +This is implementation of KQL query suggestions diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json similarity index 100% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts similarity index 92% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts index 5e562ae63e91b..c1c44f1f55548 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { setupGetConjunctionSuggestions } from './conjunction'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx similarity index 67% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx index 7efc2ea193abe..345f9f8051e5d 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -16,17 +17,17 @@ import { const bothArgumentsText = ( ); const oneOrMoreArgumentsText = ( ); @@ -34,20 +35,20 @@ const conjunctions: Record = { and: (

{bothArgumentsText}, }} description="Full text: ' Requires both arguments to be true'. See - 'xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText' for 'both arguments' part." + 'data.kueryAutocomplete.andOperatorDescription.bothArgumentsText' for 'both arguments' part." />

), or: (

= { ), }} description="Full text: 'Requires one or more arguments to be true'. See - 'xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText' for 'one or more arguments' part." + 'data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText' for 'one or more arguments' part." />

), diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.test.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.test.ts similarity index 97% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.test.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.test.ts index afc55d13af9d9..f1eced06a33ea 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.test.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.tsx b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.tsx similarity index 92% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.tsx rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.tsx index ac6f7de888320..5cafca168dfa2 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.tsx +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -22,7 +23,7 @@ const getDescription = (field: IFieldType) => { return (

{field.name} }} /> diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/index.ts similarity index 85% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/index.ts index 8b36480a35b17..c5c1626ae74f6 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { CoreSetup } from 'kibana/public'; @@ -17,6 +18,7 @@ import { QuerySuggestion, QuerySuggestionGetFnArgs, QuerySuggestionGetFn, + DataPublicPluginStart, } from '../../../../../../../src/plugins/data/public'; const cursorSymbol = '@kuery-cursor@'; @@ -26,7 +28,9 @@ const dedup = (suggestions: QuerySuggestion[]): QuerySuggestion[] => export const KUERY_LANGUAGE_NAME = 'kuery'; -export const setupKqlQuerySuggestionProvider = (core: CoreSetup): QuerySuggestionGetFn => { +export const setupKqlQuerySuggestionProvider = ( + core: CoreSetup +): QuerySuggestionGetFn => { const providers = { field: setupGetFieldSuggestions(core), value: setupGetValueSuggestions(core), diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts similarity index 92% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts index 0173617a99b1b..933449e779ef7 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { escapeQuotes, escapeKuery } from './escape_kuery'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts similarity index 85% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts index 901e61bde455d..54f03803a893e 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { flow } from 'lodash'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.test.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.test.ts similarity index 95% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.test.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.test.ts index bd021b0d0dac5..4debbc0843d51 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.test.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.tsx b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.tsx similarity index 65% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.tsx rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.tsx index cfe935e4b1990..618e33ddf345a 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.tsx +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -15,44 +16,44 @@ import { QuerySuggestionTypes } from '../../../../../../../src/plugins/data/publ const equalsText = ( ); const lessThanOrEqualToText = ( ); const greaterThanOrEqualToText = ( ); const lessThanText = ( ); const greaterThanText = ( ); const existsText = ( ); @@ -60,11 +61,11 @@ const operators = { ':': { description: ( {equalsText} }} description="Full text: 'equals some value'. See - 'xpack.data.kueryAutocomplete.equalOperatorDescription.equalsText' for 'equals' part." + 'data.kueryAutocomplete.equalOperatorDescription.equalsText' for 'equals' part." /> ), fieldTypes: [ @@ -83,7 +84,7 @@ const operators = { '<=': { description: ( ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -99,7 +100,7 @@ const operators = { '>=': { description: ( ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -115,11 +116,11 @@ const operators = { '<': { description: ( {lessThanText} }} description="Full text: 'is less than some value'. See - 'xpack.data.kueryAutocomplete.lessThanOperatorDescription.lessThanText' for 'less than' part." + 'data.kueryAutocomplete.lessThanOperatorDescription.lessThanText' for 'less than' part." /> ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -127,13 +128,13 @@ const operators = { '>': { description: ( {greaterThanText}, }} description="Full text: 'is greater than some value'. See - 'xpack.data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText' for 'greater than' part." + 'data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText' for 'greater than' part." /> ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -141,11 +142,11 @@ const operators = { ': *': { description: ( {existsText} }} description="Full text: 'exists in any form'. See - 'xpack.data.kueryAutocomplete.existOperatorDescription.existsText' for 'exists' part." + 'data.kueryAutocomplete.existOperatorDescription.existsText' for 'exists' part." /> ), fieldTypes: undefined, diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts similarity index 92% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts index aa236a45fa93c..f72fb75684105 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { sortPrefixFirst } from './sort_prefix_first'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts similarity index 76% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts index c344197641ef4..25bc32d47f338 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { partition } from 'lodash'; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/types.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts similarity index 65% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/types.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts index b5abdbee51832..48e87a73f3671 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/types.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts @@ -1,17 +1,19 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { CoreSetup } from 'kibana/public'; import { + DataPublicPluginStart, KueryNode, QuerySuggestionBasic, QuerySuggestionGetFnArgs, } from '../../../../../../../src/plugins/data/public'; export type KqlQuerySuggestionProvider = ( - core: CoreSetup + core: CoreSetup ) => (querySuggestionsGetFnArgs: QuerySuggestionGetFnArgs, kueryNode: KueryNode) => Promise; diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.test.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.test.ts similarity index 93% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.test.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.test.ts index 5744dad43dcdd..c434d9a8ef365 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.test.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.test.ts @@ -1,15 +1,15 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { setupGetValueSuggestions } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { QuerySuggestionGetFnArgs, KueryNode } from '../../../../../../../src/plugins/data/public'; -import { setAutocompleteService } from '../../../services'; const mockKueryNode = (kueryNode: Partial) => (kueryNode as unknown) as KueryNode; @@ -19,11 +19,6 @@ describe('Kuery value suggestions', () => { let autocompleteServiceMock: any; beforeEach(() => { - getSuggestions = setupGetValueSuggestions(coreMock.createSetup()); - querySuggestionsArgs = ({ - indexPatterns: [indexPatternResponse], - } as unknown) as QuerySuggestionGetFnArgs; - autocompleteServiceMock = { getValueSuggestions: jest.fn(({ field }) => { let res: any[]; @@ -40,7 +35,16 @@ describe('Kuery value suggestions', () => { return Promise.resolve(res); }), }; - setAutocompleteService(autocompleteServiceMock); + + const coreSetup = coreMock.createSetup({ + pluginStartContract: { + autocomplete: autocompleteServiceMock, + }, + }); + getSuggestions = setupGetValueSuggestions(coreSetup); + querySuggestionsArgs = ({ + indexPatterns: [indexPatternResponse], + } as unknown) as QuerySuggestionGetFnArgs; jest.clearAllMocks(); }); diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.ts similarity index 79% rename from x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts rename to src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.ts index 92fd4d7b71bdc..f8fc9d165fc6b 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -1,15 +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. + * 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 { flatten } from 'lodash'; +import { CoreSetup } from 'kibana/public'; import { escapeQuotes } from './lib/escape_kuery'; import { KqlQuerySuggestionProvider } from './types'; -import { getAutocompleteService } from '../../../services'; import { + DataPublicPluginStart, IFieldType, IIndexPattern, QuerySuggestion, @@ -26,7 +28,12 @@ const wrapAsSuggestions = (start: number, end: number, query: string, values: st end, })); -export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => { +export const setupGetValueSuggestions: KqlQuerySuggestionProvider = ( + core: CoreSetup +) => { + const autoCompleteServicePromise = core + .getStartServices() + .then(([_, __, dataStart]) => dataStart.autocomplete); return async ( { indexPatterns, boolFilter, useTimeRange, signal }, { start, end, prefix, suffix, fieldName, nestedPath } @@ -41,7 +48,7 @@ export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => { }); const query = `${prefix}${suffix}`.trim(); - const { getValueSuggestions } = getAutocompleteService(); + const { getValueSuggestions } = await autoCompleteServicePromise; const data = await Promise.all( indexPatternFieldEntries.map(([indexPattern, field]) => diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index a3676c5116927..573820890de71 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -17,7 +17,6 @@ export type Setup = jest.Mocked>; export type Start = jest.Mocked>; const automcompleteSetupMock: jest.Mocked = { - addQuerySuggestionProvider: jest.fn(), getQuerySuggestions: jest.fn(), }; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5dc5a8ab2ce93..746d035e9bfb6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -23,11 +23,12 @@ import * as CSS from 'csstype'; import { Datatable as Datatable_2 } from 'src/plugins/expressions'; import { Datatable as Datatable_3 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; -import { DatatableColumnType } from 'src/plugins/expressions/common'; +import { DatatableColumnType as DatatableColumnType_2 } from 'src/plugins/expressions/common'; import { DetailedPeerCertificate } from 'tls'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { estypes } from '@elastic/elasticsearch'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; @@ -84,16 +85,14 @@ import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { SavedObject } from 'kibana/server'; -import { SavedObject as SavedObject_2 } from 'src/core/server'; +import { SavedObject } from 'src/core/server'; +import { SavedObject as SavedObject_2 } from 'kibana/server'; import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsUpdateResponse } from 'kibana/server'; import { SchemaTypeError } from '@kbn/config-schema'; -import { Search } from '@elastic/elasticsearch/api/requestParams'; -import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { StartServicesAccessor } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; @@ -189,7 +188,7 @@ export class AggConfig { // @deprecated (undocumented) toJSON(): AggConfigSerialized; // Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts - toSerializedFieldFormat(): {} | Ensure, SerializableState>; + toSerializedFieldFormat(): {} | Ensure, SerializableState_2>; // (undocumented) get type(): IAggType; set type(type: IAggType); @@ -273,9 +272,9 @@ export type AggConfigSerialized = Ensure<{ type: string; enabled?: boolean; id?: string; - params?: {} | SerializableState; + params?: {} | SerializableState_2; schema?: string; -}, SerializableState>; +}, SerializableState_2>; // Warning: (ae-missing-release-tag) "AggFunctionsMapping" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1128,7 +1127,7 @@ export interface IEsSearchRequest extends IKibanaSearchRequest = IKibanaSearchResponse>; +export type IEsSearchResponse = IKibanaSearchResponse>; // Warning: (ae-missing-release-tag) "IFieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1605,7 +1604,7 @@ export class IndexPatternsService { // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts @@ -1617,7 +1616,7 @@ export class IndexPatternsService { }>>; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; - savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; + savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; setDefault: (id: string, force?: boolean) => Promise; updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; } @@ -2323,8 +2322,6 @@ export interface SearchError { // @public (undocumented) export class SearchInterceptor { constructor(deps: SearchInterceptorDeps); - // @internal - protected abortController: AbortController; // @internal (undocumented) protected application: CoreStart['application']; // (undocumented) @@ -2335,22 +2332,12 @@ export class SearchInterceptor { // Warning: (ae-forgotten-export) The symbol "AbortError" needs to be exported by the entry point index.d.ts // // (undocumented) - protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; + protected handleSearchError(e: KibanaServerError | AbortError, options?: ISearchOptions, isTimeout?: boolean): Error; // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) protected runSearch(request: IKibanaSearchRequest, options?: ISearchOptions): Promise; search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; - // @internal (undocumented) - protected setupAbortSignal({ abortSignal, timeout, }: { - abortSignal?: AbortSignal; - timeout?: number; - }): { - timeoutSignal: AbortSignal; - combinedSignal: AbortSignal; - cleanup: () => void; - abort: () => void; - }; // (undocumented) showError(e: Error): void; } @@ -2415,9 +2402,9 @@ export class SearchSource { createChild(options?: {}): SearchSource; createCopy(): SearchSource; destroy(): void; - fetch$(options?: ISearchOptions): import("rxjs").Observable>; + fetch$(options?: ISearchOptions): import("rxjs").Observable>; // @deprecated - fetch(options?: ISearchOptions): Promise>; + fetch(options?: ISearchOptions): Promise>; getField(field: K, recurse?: boolean): SearchSourceFields[K]; getFields(): SearchSourceFields; getId(): string; @@ -2705,7 +2692,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:55:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/expressions/es_raw_response.ts b/src/plugins/data/public/search/expressions/es_raw_response.ts index 6b44a7afb6d67..2d12af017d88c 100644 --- a/src/plugins/data/public/search/expressions/es_raw_response.ts +++ b/src/plugins/data/public/search/expressions/es_raw_response.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ExpressionTypeDefinition } from '../../../../expressions/common'; const name = 'es_raw_response'; export interface EsRawResponse { type: typeof name; - body: SearchResponse; + body: estypes.SearchResponse; } // flattens elasticsearch object into table rows @@ -46,11 +46,11 @@ function flatten(obj: any, keyPrefix = '') { } } -const parseRawDocs = (hits: SearchResponse['hits']) => { +const parseRawDocs = (hits: estypes.SearchResponse['hits']) => { return hits.hits.map((hit) => hit.fields || hit._source).filter((hit) => hit); }; -const convertResult = (body: SearchResponse) => { +const convertResult = (body: estypes.SearchResponse) => { return !body.aggregations ? parseRawDocs(body.hits) : flatten(body.aggregations); }; diff --git a/src/plugins/data/public/search/expressions/kibana_context.ts b/src/plugins/data/public/search/expressions/kibana_context.ts new file mode 100644 index 0000000000000..e7ce8edf3080a --- /dev/null +++ b/src/plugins/data/public/search/expressions/kibana_context.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { StartServicesAccessor } from 'src/core/public'; +import { getKibanaContextFn } from '../../../common/search/expressions'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +import { SavedObjectsClientCommon } from '../../../common/index_patterns'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getKibanaContext({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getKibanaContextFn(async () => { + const [core] = await getStartServices(); + return { + savedObjectsClient: (core.savedObjects.client as unknown) as SavedObjectsClientCommon, + }; + }); +} diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx index 00d5b11089d62..57ee5737e50a2 100644 --- a/src/plugins/data/public/search/fetch/handle_response.tsx +++ b/src/plugins/data/public/search/fetch/handle_response.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ShardFailureOpenModalButton } from '../../ui/shard_failure_modal'; import { toMountPoint } from '../../../../kibana_react/public'; import { getNotifications } from '../../services'; import { SearchRequest } from '..'; -export function handleResponse(request: SearchRequest, response: SearchResponse) { +export function handleResponse(request: SearchRequest, response: estypes.SearchResponse) { if (response.timed_out) { getNotifications().toasts.addWarning({ title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index f5a2dc0571fdc..3df2313f83798 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -7,7 +7,7 @@ */ import { memoize } from 'lodash'; -import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; +import { BehaviorSubject, throwError, defer, from, Observable } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; @@ -30,11 +30,7 @@ import { getHttpError, } from './errors'; import { toMountPoint } from '../../../kibana_react/public'; -import { - AbortError, - getCombinedAbortSignal, - KibanaServerError, -} from '../../../kibana_utils/public'; +import { AbortError, KibanaServerError } from '../../../kibana_utils/public'; import { ISessionService } from './session'; export interface SearchInterceptorDeps { @@ -48,12 +44,6 @@ export interface SearchInterceptorDeps { } export class SearchInterceptor { - /** - * `abortController` used to signal all searches to abort. - * @internal - */ - protected abortController = new AbortController(); - /** * Observable that emits when the number of pending requests changes. * @internal @@ -98,10 +88,10 @@ export class SearchInterceptor { */ protected handleSearchError( e: KibanaServerError | AbortError, - timeoutSignal: AbortSignal, - options?: ISearchOptions + options?: ISearchOptions, + isTimeout?: boolean ): Error { - if (timeoutSignal.aborted || e.message === 'Request timed out') { + if (isTimeout || e.message === 'Request timed out') { // Handle a client or a server side timeout const err = new SearchTimeoutError(e, this.getTimeoutMode()); @@ -154,60 +144,6 @@ export class SearchInterceptor { ); } - /** - * @internal - */ - protected setupAbortSignal({ - abortSignal, - timeout, - }: { - abortSignal?: AbortSignal; - timeout?: number; - }) { - // Schedule this request to automatically timeout after some interval - const timeoutController = new AbortController(); - const { signal: timeoutSignal } = timeoutController; - const timeout$ = timeout ? timer(timeout) : NEVER; - const subscription = timeout$.subscribe(() => { - this.deps.usageCollector?.trackQueryTimedOut(); - timeoutController.abort(); - }); - - const selfAbortController = new AbortController(); - - // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: - // 1. The internal abort controller aborts - // 2. The request times out - // 3. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks - // in the current session - // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) - const signals = [ - this.abortController.signal, - timeoutSignal, - selfAbortController.signal, - ...(abortSignal ? [abortSignal] : []), - ]; - - const { signal: combinedSignal, cleanup: cleanupCombinedSignal } = getCombinedAbortSignal( - signals - ); - const cleanup = () => { - subscription.unsubscribe(); - combinedSignal.removeEventListener('abort', cleanup); - cleanupCombinedSignal(); - }; - combinedSignal.addEventListener('abort', cleanup); - - return { - timeoutSignal, - combinedSignal, - cleanup, - abort: () => { - selfAbortController.abort(); - }, - }; - } - private showTimeoutErrorToast = (e: SearchTimeoutError, sessionId?: string) => { this.deps.toasts.addDanger({ title: 'Timed out', @@ -245,25 +181,21 @@ export class SearchInterceptor { */ public search( request: IKibanaSearchRequest, - options?: ISearchOptions + options: ISearchOptions = {} ): Observable { // Defer the following logic until `subscribe` is actually called return defer(() => { - if (options?.abortSignal?.aborted) { + if (options.abortSignal?.aborted) { return throwError(new AbortError()); } - const { timeoutSignal, combinedSignal, cleanup } = this.setupAbortSignal({ - abortSignal: options?.abortSignal, - }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe( + return from(this.runSearch(request, options)).pipe( catchError((e: Error | AbortError) => { - return throwError(this.handleSearchError(e, timeoutSignal, options)); + return throwError(this.handleSearchError(e, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); - cleanup(); }) ); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 94fa5b7230f69..a3acd775ee892 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -21,7 +21,6 @@ import { handleResponse } from './fetch'; import { kibana, kibanaContext, - kibanaContextFunction, ISearchGeneric, SearchSourceDependencies, SearchSourceService, @@ -52,6 +51,7 @@ import { import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { DataPublicPluginStart, DataStartDependencies } from '../types'; import { NowProviderInternalContract } from '../now_provider'; +import { getKibanaContext } from './expressions/kibana_context'; /** @internal */ export interface SearchServiceSetupDependencies { @@ -110,7 +110,11 @@ export class SearchService implements Plugin { }) ); expressions.registerFunction(kibana); - expressions.registerFunction(kibanaContextFunction); + expressions.registerFunction( + getKibanaContext({ getStartServices } as { + getStartServices: StartServicesAccessor; + }) + ); expressions.registerFunction(luceneFunction); expressions.registerFunction(kqlFunction); expressions.registerFunction(kibanaTimerangeFunction); diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index 8ee44cb2ca4ef..18d32463864e3 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -9,7 +9,7 @@ import { BehaviorSubject } from 'rxjs'; import { ISessionsClient } from './sessions_client'; import { ISessionService } from './session_service'; -import { SearchSessionState } from './search_session_state'; +import { SearchSessionState, SessionMeta } from './search_session_state'; export function getSessionsClientMock(): jest.Mocked { return { @@ -31,7 +31,9 @@ export function getSessionServiceMock(): jest.Mocked { getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), state$: new BehaviorSubject(SearchSessionState.None).asObservable(), - searchSessionName$: new BehaviorSubject(undefined).asObservable(), + sessionMeta$: new BehaviorSubject({ + state: SearchSessionState.None, + }).asObservable(), renameCurrentSession: jest.fn(), trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), diff --git a/src/plugins/data/public/search/session/search_session_state.ts b/src/plugins/data/public/search/session/search_session_state.ts index e58e1062091bf..bf9036d361a8f 100644 --- a/src/plugins/data/public/search/session/search_session_state.ts +++ b/src/plugins/data/public/search/session/search_session_state.ts @@ -7,6 +7,7 @@ */ import uuid from 'uuid'; +import deepEqual from 'fast-deep-equal'; import { Observable } from 'rxjs'; import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; import { createStateContainer, StateContainer } from '../../../../kibana_utils/public'; @@ -107,9 +108,19 @@ export interface SessionStateInternal { isCanceled: boolean; /** - * Start time of current session + * Start time of the current session (from browser perspective) */ startTime?: Date; + + /** + * Time when all the searches from the current session are completed (from browser perspective) + */ + completedTime?: Date; + + /** + * Time when the session was canceled by user, by hitting "stop" + */ + canceledTime?: Date; } const createSessionDefaultState: < @@ -170,12 +181,15 @@ export const sessionPureTransitions: SessionPureTransitions = { ...state, isStarted: true, pendingSearches: state.pendingSearches.concat(search), + completedTime: undefined, }; }, unTrackSearch: (state) => (search) => { + const pendingSearches = state.pendingSearches.filter((s) => s !== search); return { ...state, - pendingSearches: state.pendingSearches.filter((s) => s !== search), + pendingSearches, + completedTime: pendingSearches.length === 0 ? new Date() : state.completedTime, }; }, cancel: (state) => () => { @@ -185,6 +199,7 @@ export const sessionPureTransitions: SessionPureTransitions = { ...state, pendingSearches: [], isCanceled: true, + canceledTime: new Date(), isStored: false, searchSessionSavedObject: undefined, }; @@ -205,11 +220,24 @@ export const sessionPureTransitions: SessionPureTransitions = { }, }; +/** + * Consolidate meta info about current seach session + * Contains both deferred properties and plain properties from state + */ +export interface SessionMeta { + state: SearchSessionState; + name?: string; + startTime?: Date; + canceledTime?: Date; + completedTime?: Date; +} + export interface SessionPureSelectors< SearchDescriptor = unknown, S = SessionStateInternal > { getState: (state: S) => () => SearchSessionState; + getMeta: (state: S) => () => SessionMeta; } export const sessionPureSelectors: SessionPureSelectors = { @@ -233,6 +261,21 @@ export const sessionPureSelectors: SessionPureSelectors = { } return SearchSessionState.None; }, + getMeta(state) { + const sessionState = this.getState(state)(); + + return () => ({ + state: sessionState, + name: state.searchSessionSavedObject?.attributes.name, + startTime: state.searchSessionSavedObject?.attributes.created + ? new Date(state.searchSessionSavedObject?.attributes.created) + : state.startTime, + completedTime: state.searchSessionSavedObject?.attributes.completed + ? new Date(state.searchSessionSavedObject?.attributes.completed) + : state.completedTime, + canceledTime: state.canceledTime, + }); + }, }; export type SessionStateContainer = StateContainer< @@ -246,9 +289,7 @@ export const createSessionStateContainer = ( ): { stateContainer: SessionStateContainer; sessionState$: Observable; - sessionStartTime$: Observable; - searchSessionSavedObject$: Observable; - searchSessionName$: Observable; + sessionMeta$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), @@ -257,33 +298,20 @@ export const createSessionStateContainer = ( freeze ? undefined : { freeze: (s) => s } ) as SessionStateContainer; - const sessionState$: Observable = stateContainer.state$.pipe( - map(() => stateContainer.selectors.getState()), - distinctUntilChanged(), - shareReplay(1) - ); - - const sessionStartTime$: Observable = stateContainer.state$.pipe( - map(() => stateContainer.get().startTime), - distinctUntilChanged(), - shareReplay(1) - ); - - const searchSessionSavedObject$ = stateContainer.state$.pipe( - map(() => stateContainer.get().searchSessionSavedObject), - distinctUntilChanged(), + const sessionMeta$: Observable = stateContainer.state$.pipe( + map(() => stateContainer.selectors.getMeta()), + distinctUntilChanged(deepEqual), shareReplay(1) ); - const searchSessionName$ = searchSessionSavedObject$.pipe( - map((savedObject) => savedObject?.attributes?.name) + const sessionState$: Observable = sessionMeta$.pipe( + map((meta) => meta.state), + distinctUntilChanged() ); return { stateContainer, sessionState$, - sessionStartTime$, - searchSessionSavedObject$, - searchSessionName$, + sessionMeta$, }; }; diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 785b9357fc895..381410574ecda 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -20,6 +20,7 @@ import { ConfigSchema } from '../../../config'; import { createSessionStateContainer, SearchSessionState, + SessionMeta, SessionStateContainer, } from './search_session_state'; import { ISessionsClient } from './sessions_client'; @@ -78,7 +79,7 @@ export class SessionService { public readonly state$: Observable; private readonly state: SessionStateContainer; - public readonly searchSessionName$: Observable; + public readonly sessionMeta$: Observable; private searchSessionInfoProvider?: SearchSessionInfoProvider; private searchSessionIndicatorUiConfig?: Partial; private subscription = new Subscription(); @@ -97,20 +98,24 @@ export class SessionService { const { stateContainer, sessionState$, - sessionStartTime$, - searchSessionName$, + sessionMeta$, } = createSessionStateContainer({ freeze: freezeState, }); this.state$ = sessionState$; this.state = stateContainer; - this.searchSessionName$ = searchSessionName$; + this.sessionMeta$ = sessionMeta$; this.subscription.add( - sessionStartTime$.subscribe((startTime) => { - if (startTime) this.nowProvider.set(startTime); - else this.nowProvider.reset(); - }) + sessionMeta$ + .pipe( + map((meta) => meta.startTime), + distinctUntilChanged() + ) + .subscribe((startTime) => { + if (startTime) this.nowProvider.set(startTime); + else this.nowProvider.reset(); + }) ); getStartServices().then(([coreStart]) => { diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx index f510420cb30e8..8e6ad4bc92c8f 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx @@ -21,14 +21,14 @@ import { EuiButtonEmpty, EuiCallOut, } from '@elastic/eui'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ShardFailureTable } from './shard_failure_table'; import { ShardFailureRequest } from './shard_failure_types'; export interface Props { onClose: () => void; request: ShardFailureRequest; - response: SearchResponse; + response: estypes.SearchResponse; title: string; } diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx index 0907d6607579f..a230378d6c3d3 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiTextAlign } from '@elastic/eui'; +import type { estypes } from '@elastic/elasticsearch'; -import { SearchResponse } from 'elasticsearch'; import { getOverlays } from '../../services'; import { toMountPoint } from '../../../../kibana_react/public'; import { ShardFailureModal } from './shard_failure_modal'; @@ -19,7 +19,7 @@ import { ShardFailureRequest } from './shard_failure_types'; // @internal export interface ShardFailureOpenModalButtonProps { request: ShardFailureRequest; - response: SearchResponse; + response: estypes.SearchResponse; title: string; } diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index a7d1471af3a77..837cff41ccd6b 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -664,96 +664,87 @@ exports[`Inspector Data View component should render single table without select hasArrow={true} id="inspectorDownloadData" isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="none" repositionOnScroll={true} > -

-
- - - - - -
+ + + + +
- +
@@ -1304,81 +1295,72 @@ exports[`Inspector Data View component should render single table without select display="inlineBlock" hasArrow={true} isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="none" > -
-
- - - -
+ + + +
-
+
@@ -1420,7 +1402,7 @@ exports[`Inspector Data View component should render single table without select > - - + + + + - + @@ -2220,96 +2193,87 @@ exports[`Inspector Data View component should support multiple datatables 1`] = hasArrow={true} id="inspectorDownloadData" isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="none" repositionOnScroll={true} > -
-
- - - - - -
+ + + + +
-
+ @@ -2885,81 +2849,72 @@ exports[`Inspector Data View component should support multiple datatables 1`] = display="inlineBlock" hasArrow={true} isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="none" > -
-
- - - -
+ + + +
-
+ @@ -3001,7 +2956,7 @@ exports[`Inspector Data View component should support multiple datatables 1`] = > - - + + + + - + diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 15511e1e9f7f2..1402416d69c96 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ -import { - ConfigDeprecationLogger, - CoreSetup, - CoreStart, - PluginConfigDescriptor, -} from 'kibana/server'; +import { AddConfigDeprecation, CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; import { get } from 'lodash'; import { configSchema, ConfigSchema } from '../config'; @@ -23,17 +18,28 @@ export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ // TODO: Remove deprecation once defaultAppId is deleted - renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), - (completeConfig: Record, rootPath: string, log: ConfigDeprecationLogger) => { + renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', { silent: true }), + ( + completeConfig: Record, + rootPath: string, + addDeprecation: AddConfigDeprecation + ) => { if ( get(completeConfig, 'kibana.defaultAppId') === undefined && get(completeConfig, 'kibana_legacy.defaultAppId') === undefined ) { return completeConfig; } - log( - `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead` - ); + addDeprecation({ + message: `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead`, + correctiveActions: { + manualSteps: [ + 'Go to Stack Management > Advanced Settings', + 'Update the "defaultRoute" setting under the General section', + 'Remove "kibana.defaultAppId" from the kibana.yml config file', + ], + }, + }); return completeConfig; }, ], diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts index 1910ba054bf8e..f072f044925bf 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts @@ -12,15 +12,9 @@ export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; /** - * Roll daily indices every 30 minutes. - * This means that, assuming a user can visit all the 44 apps we can possibly report - * in the 3 minutes interval the browser reports to the server, up to 22 users can have the same - * behaviour and we wouldn't need to paginate in the transactional documents (less than 10k docs). - * - * Based on a more normal expected use case, the users could visit up to 5 apps in those 3 minutes, - * allowing up to 200 users before reaching the limit. + * Roll daily indices every 24h */ -export const ROLL_DAILY_INDICES_INTERVAL = 30 * 60 * 1000; +export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; /** * Start rolling indices after 5 minutes up diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts index 676f5fddc16e1..2d2d07d9d1894 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts @@ -7,3 +7,4 @@ */ export { registerApplicationUsageCollector } from './telemetry_application_usage_collector'; +export { rollDailyData as migrateTransactionalDocs } from './rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts similarity index 51% rename from src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts index 7d86bc41e0b90..5acd1fb9c9c3a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts @@ -6,21 +6,16 @@ * Side Public License, v 1. */ -import { rollDailyData, rollTotals } from './rollups'; -import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; -import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; -import { SavedObjectsErrorHelpers } from '../../../../../core/server'; -import { - SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './saved_objects_types'; +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../../core/server/mocks'; +import { SavedObjectsErrorHelpers } from '../../../../../../core/server'; +import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TRANSACTIONAL_TYPE } from '../saved_objects_types'; +import { rollDailyData } from './daily'; describe('rollDailyData', () => { const logger = loggingSystemMock.createLogger(); - test('returns undefined if no savedObjectsClient initialised yet', async () => { - await expect(rollDailyData(logger, undefined)).resolves.toBe(undefined); + test('returns false if no savedObjectsClient initialised yet', async () => { + await expect(rollDailyData(logger, undefined)).resolves.toBe(false); }); test('handle empty results', async () => { @@ -33,7 +28,7 @@ describe('rollDailyData', () => { throw new Error(`Unexpected type [${type}]`); } }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); expect(savedObjectClient.get).not.toBeCalled(); expect(savedObjectClient.bulkCreate).not.toBeCalled(); expect(savedObjectClient.delete).not.toBeCalled(); @@ -101,7 +96,7 @@ describe('rollDailyData', () => { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); expect(savedObjectClient.get).toHaveBeenCalledTimes(2); expect(savedObjectClient.get).toHaveBeenNthCalledWith( 1, @@ -196,7 +191,7 @@ describe('rollDailyData', () => { throw new Error('Something went terribly wrong'); }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(false); expect(savedObjectClient.get).toHaveBeenCalledTimes(1); expect(savedObjectClient.get).toHaveBeenCalledWith( SAVED_OBJECTS_DAILY_TYPE, @@ -206,185 +201,3 @@ describe('rollDailyData', () => { expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); }); }); - -describe('rollTotals', () => { - const logger = loggingSystemMock.createLogger(); - - test('returns undefined if no savedObjectsClient initialised yet', async () => { - await expect(rollTotals(logger, undefined)).resolves.toBe(undefined); - }); - - test('handle empty results', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_DAILY_TYPE: - case SAVED_OBJECTS_TOTAL_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); - }); - - test('migrate some documents', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_DAILY_TYPE: - return { - saved_objects: [ - { - id: 'appId-2:2020-01-01', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-2', - timestamp: '2020-01-01T10:31:00.000Z', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'appId-1:2020-01-01', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 2.5, - numberOfClicks: 2, - }, - }, - { - id: 'appId-1:2020-01-01:viewId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - viewId: 'viewId-1', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }, - ], - total: 3, - page, - per_page: perPage, - }; - case SAVED_OBJECTS_TOTAL_TYPE: - return { - saved_objects: [ - { - id: 'appId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'appId-1___viewId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - viewId: 'viewId-1', - minutesOnScreen: 4, - numberOfClicks: 2, - }, - }, - { - id: 'appId-2___viewId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-2', - viewId: 'viewId-1', - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }, - ], - total: 3, - page, - per_page: perPage, - }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-1', - attributes: { - appId: 'appId-1', - viewId: MAIN_APP_DEFAULT_VIEW_ID, - minutesOnScreen: 3.0, - numberOfClicks: 3, - }, - }, - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-1___viewId-1', - attributes: { - appId: 'appId-1', - viewId: 'viewId-1', - minutesOnScreen: 5.0, - numberOfClicks: 3, - }, - }, - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-2___viewId-1', - attributes: { - appId: 'appId-2', - viewId: 'viewId-1', - minutesOnScreen: 1.0, - numberOfClicks: 1, - }, - }, - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-2', - attributes: { - appId: 'appId-2', - viewId: MAIN_APP_DEFAULT_VIEW_ID, - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(3); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId-2:2020-01-01' - ); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId-1:2020-01-01' - ); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId-1:2020-01-01:viewId-1' - ); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts similarity index 55% rename from src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts rename to src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts index df7e7662b49cf..a7873c7d5dfe9 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts @@ -6,18 +6,20 @@ * Side Public License, v 1. */ -import { ISavedObjectsRepository, SavedObject, Logger } from 'kibana/server'; import moment from 'moment'; +import type { Logger } from '@kbn/logging'; +import { + ISavedObjectsRepository, + SavedObject, + SavedObjectsErrorHelpers, +} from '../../../../../../core/server'; +import { getDailyId } from '../../../../../usage_collection/common/application_usage'; import { ApplicationUsageDaily, - ApplicationUsageTotal, ApplicationUsageTransactional, SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TOTAL_TYPE, SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './saved_objects_types'; -import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; -import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; +} from '../saved_objects_types'; /** * For Rolling the daily data, we only care about the stored attributes and the version (to avoid overwriting via concurrent requests) @@ -27,18 +29,17 @@ type ApplicationUsageDailyWithVersion = Pick< 'version' | 'attributes' >; -export function serializeKey(appId: string, viewId: string) { - return `${appId}___${viewId}`; -} - /** * Aggregates all the transactional events into daily aggregates * @param logger * @param savedObjectsClient */ -export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { +export async function rollDailyData( + logger: Logger, + savedObjectsClient?: ISavedObjectsRepository +): Promise { if (!savedObjectsClient) { - return; + return false; } try { @@ -58,10 +59,7 @@ export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedO } = doc; const dayId = moment(timestamp).format('YYYY-MM-DD'); - const dailyId = - !viewId || viewId === MAIN_APP_DEFAULT_VIEW_ID - ? `${appId}:${dayId}` - : `${appId}:${dayId}:${viewId}`; + const dailyId = getDailyId({ dayId, appId, viewId }); const existingDoc = toCreate.get(dailyId) || @@ -103,9 +101,11 @@ export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedO } } } while (toCreate.size > 0); + return true; } catch (err) { logger.debug(`Failed to rollup transactional to daily entries`); logger.debug(err); + return false; } } @@ -125,7 +125,11 @@ async function getDailyDoc( dayId: string ): Promise { try { - return await savedObjectsClient.get(SAVED_OBJECTS_DAILY_TYPE, id); + const { attributes, version } = await savedObjectsClient.get( + SAVED_OBJECTS_DAILY_TYPE, + id + ); + return { attributes, version }; } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return { @@ -142,91 +146,3 @@ async function getDailyDoc( throw err; } } - -/** - * Moves all the daily documents into aggregated "total" documents as we don't care about any granularity after 90 days - * @param logger - * @param savedObjectsClient - */ -export async function rollTotals(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { - if (!savedObjectsClient) { - return; - } - - try { - const [ - { saved_objects: rawApplicationUsageTotals }, - { saved_objects: rawApplicationUsageDaily }, - ] = await Promise.all([ - savedObjectsClient.find({ - perPage: 10000, - type: SAVED_OBJECTS_TOTAL_TYPE, - }), - savedObjectsClient.find({ - perPage: 10000, - type: SAVED_OBJECTS_DAILY_TYPE, - filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.timestamp < now-90d`, - }), - ]); - - const existingTotals = rawApplicationUsageTotals.reduce( - ( - acc, - { - attributes: { appId, viewId = MAIN_APP_DEFAULT_VIEW_ID, numberOfClicks, minutesOnScreen }, - } - ) => { - const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); - - return { - ...acc, - // No need to sum because there should be 1 document per appId only - [key]: { appId, viewId, numberOfClicks, minutesOnScreen }, - }; - }, - {} as Record< - string, - { appId: string; viewId: string; minutesOnScreen: number; numberOfClicks: number } - > - ); - - const totals = rawApplicationUsageDaily.reduce((acc, { attributes }) => { - const { - appId, - viewId = MAIN_APP_DEFAULT_VIEW_ID, - numberOfClicks, - minutesOnScreen, - } = attributes; - const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); - const existing = acc[key] || { minutesOnScreen: 0, numberOfClicks: 0 }; - - return { - ...acc, - [key]: { - appId, - viewId, - numberOfClicks: numberOfClicks + existing.numberOfClicks, - minutesOnScreen: minutesOnScreen + existing.minutesOnScreen, - }, - }; - }, existingTotals); - - await Promise.all([ - Object.entries(totals).length && - savedObjectsClient.bulkCreate( - Object.entries(totals).map(([id, entry]) => ({ - type: SAVED_OBJECTS_TOTAL_TYPE, - id, - attributes: entry, - })), - { overwrite: true } - ), - ...rawApplicationUsageDaily.map( - ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_DAILY_TYPE, id) // There is no bulkDelete :( - ), - ]); - } catch (err) { - logger.debug(`Failed to rollup daily entries to totals`); - logger.debug(err); - } -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts new file mode 100644 index 0000000000000..8f3d83613aa9d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { rollDailyData } from './daily'; +export { rollTotals } from './total'; +export { serializeKey } from './utils'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts new file mode 100644 index 0000000000000..9fea955ab5d8a --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright 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 { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../../core/server/mocks'; +import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../../usage_collection/common/constants'; +import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TOTAL_TYPE } from '../saved_objects_types'; +import { rollTotals } from './total'; + +describe('rollTotals', () => { + const logger = loggingSystemMock.createLogger(); + + test('returns undefined if no savedObjectsClient initialised yet', async () => { + await expect(rollTotals(logger, undefined)).resolves.toBe(undefined); + }); + + test('handle empty results', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_DAILY_TYPE: + case SAVED_OBJECTS_TOTAL_TYPE: + return { saved_objects: [], total: 0, page, per_page: perPage }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); + }); + + test('migrate some documents', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_DAILY_TYPE: + return { + saved_objects: [ + { + id: 'appId-2:2020-01-01', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-2', + timestamp: '2020-01-01T10:31:00.000Z', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + { + id: 'appId-1:2020-01-01', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + timestamp: '2020-01-01T11:31:00.000Z', + minutesOnScreen: 2.5, + numberOfClicks: 2, + }, + }, + { + id: 'appId-1:2020-01-01:viewId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + viewId: 'viewId-1', + timestamp: '2020-01-01T11:31:00.000Z', + minutesOnScreen: 1, + numberOfClicks: 1, + }, + }, + ], + total: 3, + page, + per_page: perPage, + }; + case SAVED_OBJECTS_TOTAL_TYPE: + return { + saved_objects: [ + { + id: 'appId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + { + id: 'appId-1___viewId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + viewId: 'viewId-1', + minutesOnScreen: 4, + numberOfClicks: 2, + }, + }, + { + id: 'appId-2___viewId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-2', + viewId: 'viewId-1', + minutesOnScreen: 1, + numberOfClicks: 1, + }, + }, + ], + total: 3, + page, + per_page: perPage, + }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-1', + attributes: { + appId: 'appId-1', + viewId: MAIN_APP_DEFAULT_VIEW_ID, + minutesOnScreen: 3.0, + numberOfClicks: 3, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-1___viewId-1', + attributes: { + appId: 'appId-1', + viewId: 'viewId-1', + minutesOnScreen: 5.0, + numberOfClicks: 3, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-2___viewId-1', + attributes: { + appId: 'appId-2', + viewId: 'viewId-1', + minutesOnScreen: 1.0, + numberOfClicks: 1, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-2', + attributes: { + appId: 'appId-2', + viewId: MAIN_APP_DEFAULT_VIEW_ID, + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + { overwrite: true } + ); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(3); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-2:2020-01-01' + ); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-1:2020-01-01' + ); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-1:2020-01-01:viewId-1' + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts new file mode 100644 index 0000000000000..e27c7b897d995 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Logger } from '@kbn/logging'; +import type { ISavedObjectsRepository } from 'kibana/server'; +import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../../usage_collection/common/constants'; +import { + ApplicationUsageDaily, + ApplicationUsageTotal, + SAVED_OBJECTS_DAILY_TYPE, + SAVED_OBJECTS_TOTAL_TYPE, +} from '../saved_objects_types'; +import { serializeKey } from './utils'; + +/** + * Moves all the daily documents into aggregated "total" documents as we don't care about any granularity after 90 days + * @param logger + * @param savedObjectsClient + */ +export async function rollTotals(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { + if (!savedObjectsClient) { + return; + } + + try { + const [ + { saved_objects: rawApplicationUsageTotals }, + { saved_objects: rawApplicationUsageDaily }, + ] = await Promise.all([ + savedObjectsClient.find({ + perPage: 10000, + type: SAVED_OBJECTS_TOTAL_TYPE, + }), + savedObjectsClient.find({ + perPage: 10000, + type: SAVED_OBJECTS_DAILY_TYPE, + filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.timestamp < now-90d`, + }), + ]); + + const existingTotals = rawApplicationUsageTotals.reduce( + ( + acc, + { + attributes: { appId, viewId = MAIN_APP_DEFAULT_VIEW_ID, numberOfClicks, minutesOnScreen }, + } + ) => { + const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); + + return { + ...acc, + // No need to sum because there should be 1 document per appId only + [key]: { appId, viewId, numberOfClicks, minutesOnScreen }, + }; + }, + {} as Record< + string, + { appId: string; viewId: string; minutesOnScreen: number; numberOfClicks: number } + > + ); + + const totals = rawApplicationUsageDaily.reduce((acc, { attributes }) => { + const { + appId, + viewId = MAIN_APP_DEFAULT_VIEW_ID, + numberOfClicks, + minutesOnScreen, + } = attributes; + const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); + const existing = acc[key] || { minutesOnScreen: 0, numberOfClicks: 0 }; + + return { + ...acc, + [key]: { + appId, + viewId, + numberOfClicks: numberOfClicks + existing.numberOfClicks, + minutesOnScreen: minutesOnScreen + existing.minutesOnScreen, + }, + }; + }, existingTotals); + + await Promise.all([ + Object.entries(totals).length && + savedObjectsClient.bulkCreate( + Object.entries(totals).map(([id, entry]) => ({ + type: SAVED_OBJECTS_TOTAL_TYPE, + id, + attributes: entry, + })), + { overwrite: true } + ), + ...rawApplicationUsageDaily.map( + ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_DAILY_TYPE, id) // There is no bulkDelete :( + ), + ]); + } catch (err) { + logger.debug(`Failed to rollup daily entries to totals`); + logger.debug(err); + } +} diff --git a/src/plugins/vis_type_timeseries/common/field_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts similarity index 73% rename from src/plugins/vis_type_timeseries/common/field_types.ts rename to src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts index f9ebc83b4a5db..8be00e6287883 100644 --- a/src/plugins/vis_type_timeseries/common/field_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts @@ -6,10 +6,6 @@ * Side Public License, v 1. */ -export enum FIELD_TYPES { - BOOLEAN = 'boolean', - DATE = 'date', - GEO = 'geo_point', - NUMBER = 'number', - STRING = 'string', +export function serializeKey(appId: string, viewId: string) { + return `${appId}___${viewId}`; } diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index 9e71b5c3b032e..f2b996f3af97a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server'; +import type { SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server'; /** * Used for accumulating the totals of all the stats older than 90d @@ -17,6 +17,7 @@ export interface ApplicationUsageTotal extends SavedObjectAttributes { minutesOnScreen: number; numberOfClicks: number; } + export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; /** @@ -25,6 +26,8 @@ export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; export interface ApplicationUsageTransactional extends ApplicationUsageTotal { timestamp: string; } + +/** @deprecated transactional type is no longer used, and only preserved for backward compatibility */ export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional'; /** @@ -62,6 +65,7 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe }); // Type for storing ApplicationUsageTransactional (declaring empty mappings because we don't use the internal fields for query/aggregations) + // Remark: this type is deprecated and only here for BWC reasons. registerType({ name: SAVED_OBJECTS_TRANSACTIONAL_TYPE, hidden: false, diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 062d751ef454c..693e9132fe536 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -7,7 +7,7 @@ */ import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; -import { ApplicationUsageTelemetryReport } from './telemetry_application_usage_collector'; +import { ApplicationUsageTelemetryReport } from './types'; const commonSchema: MakeSchemaFrom = { appId: { type: 'keyword', _meta: { description: 'The application being tracked' } }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index 3e8434d446033..f1b21af5506e6 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -11,74 +11,99 @@ import { Collector, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; -import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; import { registerApplicationUsageCollector, transformByApplicationViews, - ApplicationUsageViews, } from './telemetry_application_usage_collector'; -import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; -import { - SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './saved_objects_types'; +import { ApplicationUsageViews } from './types'; -describe('telemetry_application_usage', () => { - jest.useFakeTimers(); +import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TOTAL_TYPE } from './saved_objects_types'; - const logger = loggingSystemMock.createLogger(); +// use fake timers to avoid triggering rollups during tests +jest.useFakeTimers(); +describe('telemetry_application_usage', () => { + let logger: ReturnType; let collector: Collector; + let usageCollectionMock: ReturnType; + let savedObjectClient: ReturnType; + let getSavedObjectClient: jest.MockedFunction<() => undefined | typeof savedObjectClient>; - const usageCollectionMock = createUsageCollectionSetupMock(); - usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = new Collector(logger, config); - return createUsageCollectionSetupMock().makeUsageCollector(config); - }); - - const getUsageCollector = jest.fn(); const registerType = jest.fn(); const mockedFetchContext = createCollectorFetchContextMock(); - beforeAll(() => - registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) - ); - afterAll(() => jest.clearAllTimers()); + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + usageCollectionMock = createUsageCollectionSetupMock(); + savedObjectClient = savedObjectsRepositoryMock.create(); + getSavedObjectClient = jest.fn().mockReturnValue(savedObjectClient); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + registerApplicationUsageCollector( + logger, + usageCollectionMock, + registerType, + getSavedObjectClient + ); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); test('registered collector is set', () => { expect(collector).not.toBeUndefined(); }); test('if no savedObjectClient initialised, return undefined', async () => { + getSavedObjectClient.mockReturnValue(undefined); + expect(collector.isReady()).toBe(false); expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); - jest.runTimersToTime(ROLL_INDICES_START); }); - test('when savedObjectClient is initialised, return something', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation( - async () => - ({ - saved_objects: [], - total: 0, - } as any) + test('calls `savedObjectsClient.find` with the correct parameters', async () => { + savedObjectClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + per_page: 20, + page: 0, + }); + + await collector.fetch(mockedFetchContext); + + expect(savedObjectClient.find).toHaveBeenCalledTimes(2); + + expect(savedObjectClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + type: SAVED_OBJECTS_TOTAL_TYPE, + }) + ); + expect(savedObjectClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + type: SAVED_OBJECTS_DAILY_TYPE, + }) ); - getUsageCollector.mockImplementation(() => savedObjectClient); + }); - jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run + test('when savedObjectClient is initialised, return something', async () => { + savedObjectClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + per_page: 20, + page: 0, + }); expect(collector.isReady()).toBe(true); expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); - test('it only gets 10k even when there are more documents (ES limitation)', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - const total = 10000; + test('it aggregates total and daily data', async () => { savedObjectClient.find.mockImplementation(async (opts) => { switch (opts.type) { case SAVED_OBJECTS_TOTAL_TYPE: @@ -95,18 +120,6 @@ describe('telemetry_application_usage', () => { ], total: 1, } as any; - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - const doc = { - id: 'test-id', - attributes: { - appId: 'appId', - timestamp: new Date().toISOString(), - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }; - const savedObjects = new Array(total).fill(doc); - return { saved_objects: savedObjects, total: total + 1 }; case SAVED_OBJECTS_DAILY_TYPE: return { saved_objects: [ @@ -125,122 +138,21 @@ describe('telemetry_application_usage', () => { } }); - getUsageCollector.mockImplementation(() => savedObjectClient); - - jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run - expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { appId: 'appId', viewId: 'main', - clicks_total: total + 1 + 10, - clicks_7_days: total + 1, - clicks_30_days: total + 1, - clicks_90_days: total + 1, - minutes_on_screen_total: (total + 1) * 0.5 + 10, - minutes_on_screen_7_days: (total + 1) * 0.5, - minutes_on_screen_30_days: (total + 1) * 0.5, - minutes_on_screen_90_days: (total + 1) * 0.5, + clicks_total: 1 + 10, + clicks_7_days: 1, + clicks_30_days: 1, + clicks_90_days: 1, + minutes_on_screen_total: 0.5 + 10, + minutes_on_screen_7_days: 0.5, + minutes_on_screen_30_days: 0.5, + minutes_on_screen_90_days: 0.5, views: [], }, }); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - id: 'appId', - type: SAVED_OBJECTS_TOTAL_TYPE, - attributes: { - appId: 'appId', - viewId: 'main', - minutesOnScreen: 10.5, - numberOfClicks: 11, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId:YYYY-MM-DD' - ); - }); - - test('old transactional data not migrated yet', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async (opts) => { - switch (opts.type) { - case SAVED_OBJECTS_TOTAL_TYPE: - case SAVED_OBJECTS_DAILY_TYPE: - return { saved_objects: [], total: 0 } as any; - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - return { - saved_objects: [ - { - id: 'test-id', - attributes: { - appId: 'appId', - timestamp: new Date(0).toISOString(), - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'test-id-2', - attributes: { - appId: 'appId', - viewId: 'main', - timestamp: new Date(0).toISOString(), - minutesOnScreen: 2, - numberOfClicks: 2, - }, - }, - { - id: 'test-id-3', - attributes: { - appId: 'appId', - viewId: 'viewId-1', - timestamp: new Date(0).toISOString(), - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }, - ], - total: 1, - }; - } - }); - - getUsageCollector.mockImplementation(() => savedObjectClient); - - expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ - appId: { - appId: 'appId', - viewId: 'main', - clicks_total: 3, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 2.5, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - views: [ - { - appId: 'appId', - viewId: 'viewId-1', - clicks_total: 1, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 1, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - }, - ], - }, - }); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index ee1b42e61a6ca..a01f1bca4f0e0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -11,57 +11,21 @@ import { timer } from 'rxjs'; import { ISavedObjectsRepository, Logger, SavedObjectsServiceSetup } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; -import { serializeKey } from './rollups'; - import { ApplicationUsageDaily, ApplicationUsageTotal, - ApplicationUsageTransactional, registerMappings, SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, } from './saved_objects_types'; import { applicationUsageSchema } from './schema'; -import { rollDailyData, rollTotals } from './rollups'; +import { rollTotals, rollDailyData, serializeKey } from './rollups'; import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_DAILY_INDICES_INTERVAL, ROLL_INDICES_START, } from './constants'; - -export interface ApplicationViewUsage { - appId: string; - viewId: string; - clicks_total: number; - clicks_7_days: number; - clicks_30_days: number; - clicks_90_days: number; - minutes_on_screen_total: number; - minutes_on_screen_7_days: number; - minutes_on_screen_30_days: number; - minutes_on_screen_90_days: number; -} - -export interface ApplicationUsageViews { - [serializedKey: string]: ApplicationViewUsage; -} - -export interface ApplicationUsageTelemetryReport { - [appId: string]: { - appId: string; - viewId: string; - clicks_total: number; - clicks_7_days: number; - clicks_30_days: number; - clicks_90_days: number; - minutes_on_screen_total: number; - minutes_on_screen_7_days: number; - minutes_on_screen_30_days: number; - minutes_on_screen_90_days: number; - views?: ApplicationViewUsage[]; - }; -} +import { ApplicationUsageTelemetryReport, ApplicationUsageViews } from './types'; export const transformByApplicationViews = ( report: ApplicationUsageViews @@ -92,6 +56,21 @@ export function registerApplicationUsageCollector( ) { registerMappings(registerType); + timer(ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL).subscribe(() => + rollTotals(logger, getSavedObjectsClient()) + ); + + const dailyRollingSub = timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe( + async () => { + const success = await rollDailyData(logger, getSavedObjectsClient()); + // we only need to roll the transactional documents once to assure BWC + // once we rolling succeeds, we can stop. + if (success) { + dailyRollingSub.unsubscribe(); + } + } + ); + const collector = usageCollection.makeUsageCollector( { type: 'application_usage', @@ -105,7 +84,6 @@ export function registerApplicationUsageCollector( const [ { saved_objects: rawApplicationUsageTotals }, { saved_objects: rawApplicationUsageDaily }, - { saved_objects: rawApplicationUsageTransactional }, ] = await Promise.all([ savedObjectsClient.find({ type: SAVED_OBJECTS_TOTAL_TYPE, @@ -115,10 +93,6 @@ export function registerApplicationUsageCollector( type: SAVED_OBJECTS_DAILY_TYPE, perPage: 10000, // We can have up to 44 apps * 91 days = 4004 docs. This limit is OK }), - savedObjectsClient.find({ - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - perPage: 10000, // If we have more than those, we won't report the rest (they'll be rolled up to the daily soon enough to become a problem) - }), ]); const applicationUsageFromTotals = rawApplicationUsageTotals.reduce( @@ -156,10 +130,7 @@ export function registerApplicationUsageCollector( const nowMinus30 = moment().subtract(30, 'days'); const nowMinus90 = moment().subtract(90, 'days'); - const applicationUsage = [ - ...rawApplicationUsageDaily, - ...rawApplicationUsageTransactional, - ].reduce( + const applicationUsage = rawApplicationUsageDaily.reduce( ( acc, { @@ -224,11 +195,4 @@ export function registerApplicationUsageCollector( ); usageCollection.registerCollector(collector); - - timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() => - rollDailyData(logger, getSavedObjectsClient()) - ); - timer(ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL).subscribe(() => - rollTotals(logger, getSavedObjectsClient()) - ); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts new file mode 100644 index 0000000000000..bef835e922d8d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/types.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. + */ + +export interface ApplicationViewUsage { + appId: string; + viewId: string; + clicks_total: number; + clicks_7_days: number; + clicks_30_days: number; + clicks_90_days: number; + minutes_on_screen_total: number; + minutes_on_screen_7_days: number; + minutes_on_screen_30_days: number; + minutes_on_screen_90_days: number; +} + +export interface ApplicationUsageViews { + [serializedKey: string]: ApplicationViewUsage; +} + +export interface ApplicationUsageTelemetryReport { + [appId: string]: { + appId: string; + viewId: string; + clicks_total: number; + clicks_7_days: number; + clicks_30_days: number; + clicks_90_days: number; + minutes_on_screen_total: number; + minutes_on_screen_7_days: number; + minutes_on_screen_30_days: number; + minutes_on_screen_90_days: number; + views?: ApplicationViewUsage[]; + }; +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts index 52ba793882a1d..42363f71ef87a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts @@ -58,6 +58,7 @@ export async function getSavedObjectsCounts( }; const { body } = await esClient.search(savedObjectCountSearchParams); const buckets: Array<{ key: string; doc_count: number }> = + // @ts-expect-error @elastic/elasticsearch Aggregate does not include `buckets` body.aggregations?.types?.buckets || []; // Initialise the object with all zeros for all the types diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 5959eb6aca4d4..41bb7c07bda7e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -412,4 +412,8 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInspectEsQueries': { + 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 fd63bb5bcaf43..c4a70f5065d8e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -31,6 +31,7 @@ export interface UsageStats { 'apm:enableSignificantTerms': boolean; 'apm:enableServiceOverview': boolean; 'observability:enableAlertingExperience': boolean; + 'observability:enableInspectEsQueries': boolean; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/kibana_utils/common/abort_utils.test.ts b/src/plugins/kibana_utils/common/abort_utils.test.ts index 1f8a1ef3d84c5..0d34a7852fb44 100644 --- a/src/plugins/kibana_utils/common/abort_utils.test.ts +++ b/src/plugins/kibana_utils/common/abort_utils.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { AbortError, abortSignalToPromise, getCombinedAbortSignal } from './abort_utils'; +import { AbortError, abortSignalToPromise } from './abort_utils'; jest.useFakeTimers(); @@ -66,91 +66,4 @@ describe('AbortUtils', () => { }); }); }); - - describe('getCombinedAbortSignal', () => { - test('should return an AbortSignal', () => { - const signal = getCombinedAbortSignal([]).signal; - expect(signal).toBeInstanceOf(AbortSignal); - }); - - test('should not abort if none of the signals abort', async () => { - const controller1 = new AbortController(); - const controller2 = new AbortController(); - setTimeout(() => controller1.abort(), 2000); - setTimeout(() => controller2.abort(), 1000); - const signal = getCombinedAbortSignal([controller1.signal, controller2.signal]).signal; - expect(signal.aborted).toBe(false); - jest.advanceTimersByTime(500); - await flushPromises(); - expect(signal.aborted).toBe(false); - }); - - test('should abort when the first signal aborts', async () => { - const controller1 = new AbortController(); - const controller2 = new AbortController(); - setTimeout(() => controller1.abort(), 2000); - setTimeout(() => controller2.abort(), 1000); - const signal = getCombinedAbortSignal([controller1.signal, controller2.signal]).signal; - expect(signal.aborted).toBe(false); - jest.advanceTimersByTime(1000); - await flushPromises(); - expect(signal.aborted).toBe(true); - }); - - test('should be aborted if any of the signals is already aborted', async () => { - const controller1 = new AbortController(); - const controller2 = new AbortController(); - controller1.abort(); - const signal = getCombinedAbortSignal([controller1.signal, controller2.signal]).signal; - expect(signal.aborted).toBe(true); - }); - - describe('cleanup listener', () => { - const createMockController = () => { - const controller = new AbortController(); - const spyAddListener = jest.spyOn(controller.signal, 'addEventListener'); - const spyRemoveListener = jest.spyOn(controller.signal, 'removeEventListener'); - return { - controller, - getTotalListeners: () => - Math.max(spyAddListener.mock.calls.length - spyRemoveListener.mock.calls.length, 0), - }; - }; - - test('cleanup should cleanup inner listeners', () => { - const controller1 = createMockController(); - const controller2 = createMockController(); - - const { cleanup } = getCombinedAbortSignal([ - controller1.controller.signal, - controller2.controller.signal, - ]); - - expect(controller1.getTotalListeners()).toBe(1); - expect(controller2.getTotalListeners()).toBe(1); - - cleanup(); - - expect(controller1.getTotalListeners()).toBe(0); - expect(controller2.getTotalListeners()).toBe(0); - }); - - test('abort should cleanup inner listeners', async () => { - const controller1 = createMockController(); - const controller2 = createMockController(); - - getCombinedAbortSignal([controller1.controller.signal, controller2.controller.signal]); - - expect(controller1.getTotalListeners()).toBe(1); - expect(controller2.getTotalListeners()).toBe(1); - - controller1.controller.abort(); - - await flushPromises(); - - expect(controller1.getTotalListeners()).toBe(0); - expect(controller2.getTotalListeners()).toBe(0); - }); - }); - }); }); diff --git a/src/plugins/kibana_utils/common/abort_utils.ts b/src/plugins/kibana_utils/common/abort_utils.ts index f4c750745a605..051f947b68c1b 100644 --- a/src/plugins/kibana_utils/common/abort_utils.ts +++ b/src/plugins/kibana_utils/common/abort_utils.ts @@ -45,32 +45,3 @@ export function abortSignalToPromise( return { promise, cleanup }; } - -/** - * Returns an `AbortSignal` that will be aborted when the first of the given signals aborts. - * - * @param signals - */ -export function getCombinedAbortSignal( - signals: AbortSignal[] -): { signal: AbortSignal; cleanup: () => void } { - const controller = new AbortController(); - let cleanup = () => {}; - - if (signals.some((signal) => signal.aborted)) { - controller.abort(); - } else { - const promises = signals.map((signal) => abortSignalToPromise(signal)); - cleanup = () => { - promises.forEach((p) => p.cleanup()); - controller.signal.removeEventListener('abort', cleanup); - }; - controller.signal.addEventListener('abort', cleanup); - Promise.race(promises.map((p) => p.promise)).catch(() => { - cleanup(); - controller.abort(); - }); - } - - return { signal: controller.signal, cleanup }; -} diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 398bf1415c005..76a7cb2855c6e 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -13,7 +13,7 @@ export * from './ui'; export * from './state_containers'; export * from './typed_json'; export * from './errors'; -export { AbortError, abortSignalToPromise, getCombinedAbortSignal } from './abort_utils'; +export { AbortError, abortSignalToPromise } from './abort_utils'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; export { url } from './url'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 9a94757cdcb7a..75c52e1301ea5 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -15,7 +15,6 @@ export { fieldWildcardFilter, fieldWildcardMatcher, Get, - getCombinedAbortSignal, JsonArray, JsonObject, JsonValue, diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index babc5c4a201ee..483c5aa92b45e 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -13,7 +13,6 @@ export { fieldWildcardFilter, fieldWildcardMatcher, Get, - getCombinedAbortSignal, Set, url, } from '../common'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index 67bbb46cfb607..bb426c91e827c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -60,7 +60,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` display="inlineBlock" hasArrow={true} isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="m" > { it('returns false if data only exists in system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( + // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { @@ -55,6 +56,7 @@ describe('checkClusterForUserData', () => { it('returns true if data exists in non-system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( + // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { @@ -85,6 +87,7 @@ describe('checkClusterForUserData', () => { ) .mockRejectedValueOnce(new Error('something terrible happened')) .mockResolvedValueOnce( + // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { @@ -95,6 +98,7 @@ describe('checkClusterForUserData', () => { }) ) .mockResolvedValueOnce( + // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { diff --git a/src/plugins/security_oss/server/check_cluster_data.ts b/src/plugins/security_oss/server/check_cluster_data.ts index c8c30196b485c..19a4145333dd0 100644 --- a/src/plugins/security_oss/server/check_cluster_data.ts +++ b/src/plugins/security_oss/server/check_cluster_data.ts @@ -14,17 +14,15 @@ export const createClusterDataCheck = () => { return async function doesClusterHaveUserData(esClient: ElasticsearchClient, log: Logger) { if (!clusterHasUserData) { try { - const indices = await esClient.cat.indices< - Array<{ index: string; ['docs.count']: string }> - >({ + const indices = await esClient.cat.indices({ format: 'json', h: ['index', 'docs.count'], }); clusterHasUserData = indices.body.some((indexCount) => { const isInternalIndex = - indexCount.index.startsWith('.') || indexCount.index.startsWith('kibana_sample_'); + indexCount.index?.startsWith('.') || indexCount.index?.startsWith('kibana_sample_'); - return !isInternalIndex && parseInt(indexCount['docs.count'], 10) > 0; + return !isInternalIndex && parseInt(indexCount['docs.count']!, 10) > 0; }); } catch (e) { log.warn(`Error encountered while checking cluster for user data: ${e}`); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 451b3ffe91535..ee96ae041dd09 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8032,6 +8032,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "observability:enableInspectEsQueries": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, @@ -9327,6 +9333,16 @@ } } }, + "vis_type_timeseries": { + "properties": { + "timeseries_use_last_value_mode_total": { + "type": "long", + "_meta": { + "description": "Number of TSVB visualizations using \"last value\" as a time range" + } + } + } + }, "vis_type_vega": { "properties": { "vega_lib_specs_total": { diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts index 437d76fe7ccf2..3f93bde1e7e62 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts @@ -8,22 +8,6 @@ import { ElasticsearchClient } from 'src/core/server'; -// This can be removed when the ES client improves the types -export interface ESClusterInfo { - cluster_uuid: string; - cluster_name: string; - version: { - number: string; - build_flavor?: string; - build_type?: string; - build_hash?: string; - build_date?: string; - build_snapshot?: boolean; - lucene_version?: string; - minimum_wire_compatibility_version?: string; - minimum_index_compatibility_version?: string; - }; -} /** * Get the cluster info from the connected cluster. * @@ -32,6 +16,6 @@ export interface ESClusterInfo { * @param {function} esClient The asInternalUser handler (exposed for testing) */ export async function getClusterInfo(esClient: ElasticsearchClient) { - const { body } = await esClient.info(); + const { body } = await esClient.info(); return body; } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index 42ccbcc46c462..c79c46072e11b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -261,14 +261,16 @@ export async function getDataTelemetry(esClient: ElasticsearchClient) { const indices = indexNames.map((name) => { const baseIndexInfo = { name, - isECS: !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type, + isECS: !!indexMappings[name]?.mappings?.properties?.ecs?.properties?.version?.type, shipper: indexMappings[name]?.mappings?._meta?.beat, packageName: indexMappings[name]?.mappings?._meta?.package?.name, managedBy: indexMappings[name]?.mappings?._meta?.managed_by, dataStreamDataset: - indexMappings[name]?.mappings?.properties.data_stream?.properties.dataset?.value, + // @ts-expect-error @elastic/elasticsearch PropertyBase doesn't decalre value + indexMappings[name]?.mappings?.properties?.data_stream?.properties?.dataset?.value, dataStreamType: - indexMappings[name]?.mappings?.properties.data_stream?.properties.type?.value, + // @ts-expect-error @elastic/elasticsearch PropertyBase doesn't decalre value + indexMappings[name]?.mappings?.properties?.data_stream?.properties?.type?.value, }; const stats = (indexStats?.indices || {})[name]; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 47c6736ff9aea..edf8dbb30809b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -7,6 +7,7 @@ */ import { merge, omit } from 'lodash'; +import type { estypes } from '@elastic/elasticsearch'; import { getLocalStats, handleLocalStats } from './get_local_stats'; import { @@ -34,35 +35,33 @@ function mockGetLocalStats(clusterInfo: any, clusterStats: any) { esClient.cluster.stats // @ts-expect-error we only care about the response body .mockResolvedValue({ body: { ...clusterStats } }); - esClient.nodes.usage.mockResolvedValue( + esClient.nodes.usage.mockResolvedValue({ // @ts-expect-error we only care about the response body - { - body: { - cluster_name: 'testCluster', - nodes: { - some_node_id: { - timestamp: 1588617023177, - since: 1588616945163, - rest_actions: { - nodes_usage_action: 1, - create_index_action: 1, - document_get_action: 1, - search_action: 19, - nodes_info_action: 36, + body: { + cluster_name: 'testCluster', + nodes: { + some_node_id: { + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + scripted_metric: { + other: 7, }, - aggregations: { - terms: { - bytes: 2, - }, - scripted_metric: { - other: 7, - }, + terms: { + bytes: 2, }, }, }, }, - } - ); + }, + }); // @ts-expect-error we only care about the response body esClient.indices.getMapping.mockResolvedValue({ body: { mappings: {} } }); // @ts-expect-error we only care about the response body @@ -188,7 +187,7 @@ describe('get_local_stats', () => { describe('handleLocalStats', () => { it('returns expected object without xpack or kibana data', () => { const result = handleLocalStats( - clusterInfo, + clusterInfo as estypes.RootNodeInfoResponse, clusterStatsWithNodesUsage, void 0, void 0, @@ -205,7 +204,7 @@ describe('get_local_stats', () => { it('returns expected object with xpack', () => { const result = handleLocalStats( - clusterInfo, + clusterInfo as estypes.RootNodeInfoResponse, clusterStatsWithNodesUsage, void 0, void 0, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 710d836576d10..67f9ebb8ff3e4 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import { StatsGetter, StatsCollectionContext, } from 'src/plugins/telemetry_collection_manager/server'; -import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; +import { getClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { getNodesUsage } from './get_nodes_usage'; @@ -27,7 +28,7 @@ import { getDataTelemetry, DATA_TELEMETRY_ID, DataTelemetryPayload } from './get */ export function handleLocalStats( // eslint-disable-next-line @typescript-eslint/naming-convention - { cluster_name, cluster_uuid, version }: ESClusterInfo, + { cluster_name, cluster_uuid, version }: estypes.RootNodeInfoResponse, { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats | undefined, dataTelemetry: DataTelemetryPayload | undefined, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts index 18c6d16447238..e46d4be540734 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts @@ -16,7 +16,7 @@ export interface NodeAggregation { // we set aggregations as an optional type because it was only added in v7.8.0 export interface NodeObj { node_id?: string; - timestamp: number; + timestamp: number | string; since: number; rest_actions: { [key: string]: number; @@ -46,9 +46,10 @@ export type NodesUsageGetter = ( export async function fetchNodesUsage( esClient: ElasticsearchClient ): Promise { - const { body } = await esClient.nodes.usage({ + const { body } = await esClient.nodes.usage({ timeout: TIMEOUT, }); + // @ts-expect-error TODO: Does the client parse `timestamp` to a Date object? Expected a number return body; } diff --git a/src/plugins/timelion/server/deprecations.ts b/src/plugins/timelion/server/deprecations.ts new file mode 100644 index 0000000000000..3d4e687f154cf --- /dev/null +++ b/src/plugins/timelion/server/deprecations.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 { + CoreStart, + SavedObjectsClient, + Logger, + GetDeprecationsContext, + DeprecationsDetails, +} from 'src/core/server'; + +export const getTimelionSheetsCount = async ( + savedObjectsClient: Pick +) => { + const { total } = await savedObjectsClient.find({ type: 'timelion-sheet', perPage: 1 }); + return total; +}; + +export const showWarningMessageIfTimelionSheetWasFound = async ( + core: CoreStart, + logger: Logger +) => { + const { savedObjects } = core; + const savedObjectsClient = savedObjects.createInternalRepository(); + const count = await getTimelionSheetsCount(savedObjectsClient); + if (count > 0) { + logger.warn( + 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' + ); + } +}; + +/** + * Deprecated since 7.0, the Timelion app will be removed in 8.0. + * To continue using your Timelion worksheets, migrate them to a dashboard. + * + * @link https://www.elastic.co/guide/en/kibana/master/timelion.html#timelion-deprecation + **/ +export async function getDeprecations({ + savedObjectsClient, +}: GetDeprecationsContext): Promise { + const deprecations: DeprecationsDetails[] = []; + const count = await getTimelionSheetsCount(savedObjectsClient); + + if (count > 0) { + deprecations.push({ + message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + documentationUrl: + 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', + level: 'warning', + correctiveActions: { + manualSteps: [ + 'Navigate to the Kibana Dashboard and click "Create dashboard".', + 'Select Timelion from the "New Visualization" window.', + 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', + 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', + 'In the toolbar, click Save.', + 'On the Save visualization window, enter the visualization Title, then click Save and return.', + ], + }, + }); + } + + return deprecations; +} diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 66348c572117d..edbba9b565ae4 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -11,30 +11,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { TimelionConfigType } from './config'; import { timelionSheetSavedObjectType } from './saved_objects'; - -/** - * Deprecated since 7.0, the Timelion app will be removed in 8.0. - * To continue using your Timelion worksheets, migrate them to a dashboard. - * - * @link https://www.elastic.co/guide/en/kibana/master/timelion.html#timelion-deprecation - **/ -const showWarningMessageIfTimelionSheetWasFound = (core: CoreStart, logger: Logger) => { - const { savedObjects } = core; - const savedObjectsClient = savedObjects.createInternalRepository(); - - savedObjectsClient - .find({ - type: 'timelion-sheet', - perPage: 1, - }) - .then( - ({ total }) => - total && - logger.warn( - 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' - ) - ); -}; +import { getDeprecations, showWarningMessageIfTimelionSheetWasFound } from './deprecations'; export class TimelionPlugin implements Plugin { private logger: Logger; @@ -47,6 +24,7 @@ export class TimelionPlugin implements Plugin { core.capabilities.registerProvider(() => ({ timelion: { save: true, + show: true, }, })); core.savedObjects.registerType(timelionSheetSavedObjectType); @@ -86,6 +64,8 @@ export class TimelionPlugin implements Plugin { schema: schema.number(), }, }); + + core.deprecations.registerDeprecations({ getDeprecations }); } start(core: CoreStart) { showWarningMessageIfTimelionSheetWasFound(core, this.logger); diff --git a/src/plugins/timelion/server/saved_objects/timelion_sheet.ts b/src/plugins/timelion/server/saved_objects/timelion_sheet.ts index 52d7f59a7c734..231e049280bb1 100644 --- a/src/plugins/timelion/server/saved_objects/timelion_sheet.ts +++ b/src/plugins/timelion/server/saved_objects/timelion_sheet.ts @@ -12,6 +12,20 @@ export const timelionSheetSavedObjectType: SavedObjectsType = { name: 'timelion-sheet', hidden: false, namespaceType: 'single', + management: { + icon: 'visTimelion', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/timelion#/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'timelion.show', + }; + }, + }, mappings: { properties: { description: { type: 'text' }, diff --git a/src/plugins/usage_collection/common/application_usage.ts b/src/plugins/usage_collection/common/application_usage.ts new file mode 100644 index 0000000000000..c9dd489000d35 --- /dev/null +++ b/src/plugins/usage_collection/common/application_usage.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 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 { MAIN_APP_DEFAULT_VIEW_ID } from './constants'; + +export const getDailyId = ({ + appId, + dayId, + viewId, +}: { + viewId: string; + appId: string; + dayId: string; +}) => { + return !viewId || viewId === MAIN_APP_DEFAULT_VIEW_ID + ? `${appId}:${dayId}` + : `${appId}:${dayId}:${viewId}`; +}; diff --git a/src/plugins/usage_collection/server/report/schema.ts b/src/plugins/usage_collection/server/report/schema.ts index 93203a33cd1e1..350ec8d90e765 100644 --- a/src/plugins/usage_collection/server/report/schema.ts +++ b/src/plugins/usage_collection/server/report/schema.ts @@ -9,6 +9,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { METRIC_TYPE } from '@kbn/analytics'; +const applicationUsageReportSchema = schema.object({ + minutesOnScreen: schema.number(), + numberOfClicks: schema.number(), + appId: schema.string(), + viewId: schema.string(), +}); + export const reportSchema = schema.object({ reportVersion: schema.maybe(schema.oneOf([schema.literal(3)])), userAgent: schema.maybe( @@ -38,17 +45,8 @@ export const reportSchema = schema.object({ }) ) ), - application_usage: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - minutesOnScreen: schema.number(), - numberOfClicks: schema.number(), - appId: schema.string(), - viewId: schema.string(), - }) - ) - ), + application_usage: schema.maybe(schema.recordOf(schema.string(), applicationUsageReportSchema)), }); export type ReportSchemaType = TypeOf; +export type ApplicationUsageReport = TypeOf; diff --git a/src/plugins/usage_collection/server/report/store_application_usage.test.ts b/src/plugins/usage_collection/server/report/store_application_usage.test.ts new file mode 100644 index 0000000000000..c4c9e5746e6cb --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_application_usage.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright 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 moment from 'moment'; +import { savedObjectsRepositoryMock } from '../../../../core/server/mocks'; +import { getDailyId } from '../../common/application_usage'; +import { storeApplicationUsage } from './store_application_usage'; +import { ApplicationUsageReport } from './schema'; + +const createReport = (parts: Partial): ApplicationUsageReport => ({ + appId: 'appId', + viewId: 'viewId', + numberOfClicks: 0, + minutesOnScreen: 0, + ...parts, +}); + +describe('storeApplicationUsage', () => { + let repository: ReturnType; + let timestamp: Date; + + beforeEach(() => { + repository = savedObjectsRepositoryMock.create(); + timestamp = new Date(); + }); + + it('does not call `repository.incrementUsageCounters` when the report list is empty', async () => { + await storeApplicationUsage(repository, [], timestamp); + expect(repository.incrementCounter).not.toHaveBeenCalled(); + }); + + it('calls `repository.incrementUsageCounters` with the correct parameters', async () => { + const report = createReport({ + appId: 'app1', + viewId: 'view1', + numberOfClicks: 2, + minutesOnScreen: 5, + }); + + await storeApplicationUsage(repository, [report], timestamp); + + expect(repository.incrementCounter).toHaveBeenCalledTimes(1); + + expect(repository.incrementCounter).toHaveBeenCalledWith( + ...expectedIncrementParams(report, timestamp) + ); + }); + + it('aggregates reports with the same appId/viewId tuple', async () => { + const report1 = createReport({ + appId: 'app1', + viewId: 'view1', + numberOfClicks: 2, + minutesOnScreen: 5, + }); + const report2 = createReport({ + appId: 'app1', + viewId: 'view2', + numberOfClicks: 1, + minutesOnScreen: 7, + }); + const report3 = createReport({ + appId: 'app1', + viewId: 'view1', + numberOfClicks: 3, + minutesOnScreen: 9, + }); + + await storeApplicationUsage(repository, [report1, report2, report3], timestamp); + + expect(repository.incrementCounter).toHaveBeenCalledTimes(2); + + expect(repository.incrementCounter).toHaveBeenCalledWith( + ...expectedIncrementParams( + { + appId: 'app1', + viewId: 'view1', + numberOfClicks: report1.numberOfClicks + report3.numberOfClicks, + minutesOnScreen: report1.minutesOnScreen + report3.minutesOnScreen, + }, + timestamp + ) + ); + expect(repository.incrementCounter).toHaveBeenCalledWith( + ...expectedIncrementParams(report2, timestamp) + ); + }); +}); + +const expectedIncrementParams = ( + { appId, viewId, minutesOnScreen, numberOfClicks }: ApplicationUsageReport, + timestamp: Date +) => { + const dayId = moment(timestamp).format('YYYY-MM-DD'); + return [ + 'application_usage_daily', + getDailyId({ appId, viewId, dayId }), + [ + { fieldName: 'numberOfClicks', incrementBy: numberOfClicks }, + { fieldName: 'minutesOnScreen', incrementBy: minutesOnScreen }, + ], + { + upsertAttributes: { + appId, + viewId, + timestamp: moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(), + }, + }, + ]; +}; diff --git a/src/plugins/usage_collection/server/report/store_application_usage.ts b/src/plugins/usage_collection/server/report/store_application_usage.ts new file mode 100644 index 0000000000000..2058b054fda8c --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_application_usage.ts @@ -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 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 moment from 'moment'; +import { Writable } from '@kbn/utility-types'; +import { ISavedObjectsRepository } from 'src/core/server'; +import { ApplicationUsageReport } from './schema'; +import { getDailyId } from '../../common/application_usage'; + +type WritableApplicationUsageReport = Writable; + +export const storeApplicationUsage = async ( + repository: ISavedObjectsRepository, + appUsages: ApplicationUsageReport[], + timestamp: Date +) => { + if (!appUsages.length) { + return; + } + + const dayId = getDayId(timestamp); + const aggregatedReports = aggregateAppUsages(appUsages); + + return Promise.allSettled( + aggregatedReports.map(async (report) => incrementUsageCounters(repository, report, dayId)) + ); +}; + +const aggregateAppUsages = (appUsages: ApplicationUsageReport[]) => { + return [ + ...appUsages + .reduce((map, appUsage) => { + const key = getKey(appUsage); + const aggregated: WritableApplicationUsageReport = map.get(key) ?? { + appId: appUsage.appId, + viewId: appUsage.viewId, + minutesOnScreen: 0, + numberOfClicks: 0, + }; + + aggregated.minutesOnScreen += appUsage.minutesOnScreen; + aggregated.numberOfClicks += appUsage.numberOfClicks; + + map.set(key, aggregated); + return map; + }, new Map()) + .values(), + ]; +}; + +const incrementUsageCounters = ( + repository: ISavedObjectsRepository, + { appId, viewId, numberOfClicks, minutesOnScreen }: WritableApplicationUsageReport, + dayId: string +) => { + const dailyId = getDailyId({ appId, viewId, dayId }); + + return repository.incrementCounter( + 'application_usage_daily', + dailyId, + [ + { fieldName: 'numberOfClicks', incrementBy: numberOfClicks }, + { fieldName: 'minutesOnScreen', incrementBy: minutesOnScreen }, + ], + { + upsertAttributes: { + appId, + viewId, + timestamp: getTimestamp(dayId), + }, + } + ); +}; + +const getKey = ({ viewId, appId }: ApplicationUsageReport) => `${appId}___${viewId}`; + +const getDayId = (timestamp: Date) => moment(timestamp).format('YYYY-MM-DD'); + +const getTimestamp = (dayId: string) => { + // Concatenating the day in YYYY-MM-DD form to T00:00:00Z to reduce the TZ effects + return moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(); +}; diff --git a/src/plugins/usage_collection/server/report/store_report.test.mocks.ts b/src/plugins/usage_collection/server/report/store_report.test.mocks.ts new file mode 100644 index 0000000000000..d151e7d7a5ddd --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_report.test.mocks.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const storeApplicationUsageMock = jest.fn(); +jest.doMock('./store_application_usage', () => ({ + storeApplicationUsage: storeApplicationUsageMock, +})); diff --git a/src/plugins/usage_collection/server/report/store_report.test.ts b/src/plugins/usage_collection/server/report/store_report.test.ts index 7174a54067246..dfcdd1f8e7e42 100644 --- a/src/plugins/usage_collection/server/report/store_report.test.ts +++ b/src/plugins/usage_collection/server/report/store_report.test.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { storeApplicationUsageMock } from './store_report.test.mocks'; + import { savedObjectsRepositoryMock } from '../../../../core/server/mocks'; import { storeReport } from './store_report'; import { ReportSchemaType } from './schema'; @@ -16,8 +18,17 @@ describe('store_report', () => { const momentTimestamp = moment(); const date = momentTimestamp.format('DDMMYYYY'); + let repository: ReturnType; + + beforeEach(() => { + repository = savedObjectsRepositoryMock.create(); + }); + + afterEach(() => { + storeApplicationUsageMock.mockReset(); + }); + test('stores report for all types of data', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); const report: ReportSchemaType = { reportVersion: 3, userAgent: { @@ -53,9 +64,9 @@ describe('store_report', () => { }, }, }; - await storeReport(savedObjectClient, report); + await storeReport(repository, report); - expect(savedObjectClient.create).toHaveBeenCalledWith( + expect(repository.create).toHaveBeenCalledWith( 'ui-metric', { count: 1 }, { @@ -63,51 +74,45 @@ describe('store_report', () => { overwrite: true, } ); - expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith( + expect(repository.incrementCounter).toHaveBeenNthCalledWith( 1, 'ui-metric', 'test-app-name:test-event-name', [{ fieldName: 'count', incrementBy: 3 }] ); - expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith( + expect(repository.incrementCounter).toHaveBeenNthCalledWith( 2, 'ui-counter', `test-app-name:${date}:${METRIC_TYPE.LOADED}:test-event-name`, [{ fieldName: 'count', incrementBy: 1 }] ); - expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith( + expect(repository.incrementCounter).toHaveBeenNthCalledWith( 3, 'ui-counter', `test-app-name:${date}:${METRIC_TYPE.CLICK}:test-event-name`, [{ fieldName: 'count', incrementBy: 2 }] ); - expect(savedObjectClient.bulkCreate).toHaveBeenNthCalledWith(1, [ - { - type: 'application_usage_transactional', - attributes: { - numberOfClicks: 3, - minutesOnScreen: 10, - appId: 'appId', - viewId: 'appId_view', - timestamp: expect.any(Date), - }, - }, - ]); + + expect(storeApplicationUsageMock).toHaveBeenCalledTimes(1); + expect(storeApplicationUsageMock).toHaveBeenCalledWith( + repository, + Object.values(report.application_usage as Record), + expect.any(Date) + ); }); test('it should not fail if nothing to store', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); const report: ReportSchemaType = { reportVersion: 3, userAgent: void 0, uiCounter: void 0, application_usage: void 0, }; - await storeReport(savedObjectClient, report); + await storeReport(repository, report); - expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); - expect(savedObjectClient.incrementCounter).not.toHaveBeenCalled(); - expect(savedObjectClient.create).not.toHaveBeenCalled(); - expect(savedObjectClient.create).not.toHaveBeenCalled(); + expect(repository.bulkCreate).not.toHaveBeenCalled(); + expect(repository.incrementCounter).not.toHaveBeenCalled(); + expect(repository.create).not.toHaveBeenCalled(); + expect(repository.create).not.toHaveBeenCalled(); }); }); diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts index c3e04990d5793..0545a54792d45 100644 --- a/src/plugins/usage_collection/server/report/store_report.ts +++ b/src/plugins/usage_collection/server/report/store_report.ts @@ -10,6 +10,7 @@ import { ISavedObjectsRepository } from 'src/core/server'; import moment from 'moment'; import { chain, sumBy } from 'lodash'; import { ReportSchemaType } from './schema'; +import { storeApplicationUsage } from './store_application_usage'; export async function storeReport( internalRepository: ISavedObjectsRepository, @@ -17,11 +18,11 @@ export async function storeReport( ) { const uiCounters = report.uiCounter ? Object.entries(report.uiCounter) : []; const userAgents = report.userAgent ? Object.entries(report.userAgent) : []; - const appUsage = report.application_usage ? Object.values(report.application_usage) : []; + const appUsages = report.application_usage ? Object.values(report.application_usage) : []; const momentTimestamp = moment(); - const timestamp = momentTimestamp.toDate(); const date = momentTimestamp.format('DDMMYYYY'); + const timestamp = momentTimestamp.toDate(); return Promise.allSettled([ // User Agent @@ -64,21 +65,6 @@ export async function storeReport( ]; }), // Application Usage - ...[ - (async () => { - if (!appUsage.length) return []; - const { saved_objects: savedObjects } = await internalRepository.bulkCreate( - appUsage.map((metric) => ({ - type: 'application_usage_transactional', - attributes: { - ...metric, - timestamp, - }, - })) - ); - - return savedObjects; - })(), - ], + storeApplicationUsage(internalRepository, appUsages, timestamp), ]); } diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index 1dcb7263c4818..35f4182a50a86 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -21,7 +21,7 @@ export const config: PluginConfigDescriptor = { renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'), renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'), renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'), - renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', true), + renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', { silent: true }), ], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.ts b/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.ts deleted file mode 100644 index c4da2085855e6..0000000000000 --- a/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { extractIndexPatterns } from './extract_index_patterns'; -import { PanelSchema } from './types'; - -describe('extractIndexPatterns(vis)', () => { - let panel: PanelSchema; - - beforeEach(() => { - panel = { - index_pattern: '*', - series: [ - { - override_index_pattern: 1, - series_index_pattern: 'example-1-*', - }, - { - override_index_pattern: 1, - series_index_pattern: 'example-2-*', - }, - ], - annotations: [{ index_pattern: 'notes-*' }, { index_pattern: 'example-1-*' }], - } as PanelSchema; - }); - - test('should return index patterns', () => { - expect(extractIndexPatterns(panel, '')).toEqual(['*', 'example-1-*', 'example-2-*', 'notes-*']); - }); -}); diff --git a/src/plugins/vis_type_timeseries/common/extract_index_patterns.ts b/src/plugins/vis_type_timeseries/common/extract_index_patterns.ts deleted file mode 100644 index c716ae7abb821..0000000000000 --- a/src/plugins/vis_type_timeseries/common/extract_index_patterns.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { uniq } from 'lodash'; -import { PanelSchema } from '../common/types'; - -export function extractIndexPatterns( - panel: PanelSchema, - defaultIndex?: PanelSchema['default_index_pattern'] -) { - const patterns: string[] = []; - - if (panel.index_pattern) { - patterns.push(panel.index_pattern); - } - - panel.series.forEach((series) => { - const indexPattern = series.series_index_pattern; - if (indexPattern && series.override_index_pattern) { - patterns.push(indexPattern); - } - }); - - if (panel.annotations) { - panel.annotations.forEach((item) => { - const indexPattern = item.index_pattern; - if (indexPattern) { - patterns.push(indexPattern); - } - }); - } - - if (patterns.length === 0 && defaultIndex) { - patterns.push(defaultIndex); - } - - return uniq(patterns).sort(); -} diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts b/src/plugins/vis_type_timeseries/common/fields_utils.test.ts new file mode 100644 index 0000000000000..d1036aab2dc3e --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/fields_utils.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { toSanitizedFieldType } from './fields_utils'; +import type { FieldSpec, RuntimeField } from '../../data/common'; + +describe('fields_utils', () => { + describe('toSanitizedFieldType', () => { + const mockedField = { + lang: 'lang', + conflictDescriptions: {}, + aggregatable: true, + name: 'name', + type: 'type', + esTypes: ['long', 'geo'], + } as FieldSpec; + + test('should sanitize fields ', async () => { + const fields = [mockedField] as FieldSpec[]; + + expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(` + Array [ + Object { + "label": "name", + "name": "name", + "type": "type", + }, + ] + `); + }); + + test('should filter runtime fields', async () => { + const fields: FieldSpec[] = [ + { + ...mockedField, + runtimeField: {} as RuntimeField, + }, + ]; + + expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); + }); + + test('should filter non-aggregatable fields', async () => { + const fields: FieldSpec[] = [ + { + ...mockedField, + aggregatable: false, + }, + ]; + + expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); + }); + + test('should filter nested fields', async () => { + const fields: FieldSpec[] = [ + { + ...mockedField, + subType: { + nested: { + path: 'path', + }, + }, + }, + ]; + expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.ts b/src/plugins/vis_type_timeseries/common/fields_utils.ts new file mode 100644 index 0000000000000..04499d5320ab8 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/fields_utils.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 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 { FieldSpec } from '../../data/common'; +import { isNestedField } from '../../data/common'; +import { SanitizedFieldType } from './types'; + +export const toSanitizedFieldType = (fields: FieldSpec[]) => { + return fields + .filter( + (field) => + // Make sure to only include mapped fields, e.g. no index pattern runtime fields + !field.runtimeField && field.aggregatable && !isNestedField(field) + ) + .map( + (field) => + ({ + name: field.name, + label: field.customLabel ?? field.name, + type: field.type, + } as SanitizedFieldType) + ); +}; 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 new file mode 100644 index 0000000000000..515fadffb6b32 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright 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 { + extractIndexPatternValues, + isStringTypeIndexPattern, + fetchIndexPattern, +} from './index_patterns_utils'; +import { PanelSchema } from './types'; +import { IndexPattern, IndexPatternsService } from '../../data/common'; + +describe('isStringTypeIndexPattern', () => { + test('should returns true on string-based index', () => { + expect(isStringTypeIndexPattern('index')).toBeTruthy(); + }); + test('should returns false on object-based index', () => { + expect(isStringTypeIndexPattern({ id: 'id' })).toBeFalsy(); + }); +}); + +describe('extractIndexPatterns', () => { + let panel: PanelSchema; + + beforeEach(() => { + panel = { + index_pattern: '*', + series: [ + { + override_index_pattern: 1, + series_index_pattern: 'example-1-*', + }, + { + override_index_pattern: 1, + series_index_pattern: 'example-2-*', + }, + ], + annotations: [{ index_pattern: 'notes-*' }, { index_pattern: 'example-1-*' }], + } as PanelSchema; + }); + + test('should return index patterns', () => { + expect(extractIndexPatternValues(panel, '')).toEqual([ + '*', + 'example-1-*', + 'example-2-*', + 'notes-*', + ]); + }); +}); + +describe('fetchIndexPattern', () => { + let mockedIndices: IndexPattern[] | []; + let indexPatternsService: IndexPatternsService; + + beforeEach(() => { + mockedIndices = []; + + indexPatternsService = ({ + getDefault: jest.fn(() => Promise.resolve({ id: 'default', title: 'index' })), + get: jest.fn(() => Promise.resolve(mockedIndices[0])), + find: jest.fn(() => Promise.resolve(mockedIndices || [])), + } as unknown) as IndexPatternsService; + }); + + test('should return default index on no input value', async () => { + const value = await fetchIndexPattern('', indexPatternsService); + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": Object { + "id": "default", + "title": "index", + }, + "indexPatternString": "index", + } + `); + }); + + describe('text-based index', () => { + test('should return the Kibana index if it exists', async () => { + mockedIndices = [ + { + id: 'indexId', + title: 'indexTitle', + }, + ] as IndexPattern[]; + + const value = await fetchIndexPattern('indexTitle', indexPatternsService); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": Object { + "id": "indexId", + "title": "indexTitle", + }, + "indexPatternString": "indexTitle", + } + `); + }); + + test('should return only indexPatternString if Kibana index does not exist', async () => { + const value = await fetchIndexPattern('indexTitle', indexPatternsService); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": undefined, + "indexPatternString": "indexTitle", + } + `); + }); + }); + + describe('object-based index', () => { + test('should return the Kibana index if it exists', async () => { + mockedIndices = [ + { + id: 'indexId', + title: 'indexTitle', + }, + ] as IndexPattern[]; + + const value = await fetchIndexPattern({ id: 'indexId' }, indexPatternsService); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": Object { + "id": "indexId", + "title": "indexTitle", + }, + "indexPatternString": "indexTitle", + } + `); + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts new file mode 100644 index 0000000000000..398d1c30ed5a7 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uniq } from 'lodash'; +import { PanelSchema, IndexPatternValue, FetchedIndexPattern } from '../common/types'; +import { IndexPatternsService } from '../../data/common'; + +export const isStringTypeIndexPattern = ( + indexPatternValue: IndexPatternValue +): indexPatternValue is string => typeof indexPatternValue === 'string'; + +export const getIndexPatternKey = (indexPatternValue: IndexPatternValue) => + isStringTypeIndexPattern(indexPatternValue) ? indexPatternValue : indexPatternValue?.id ?? ''; + +export const extractIndexPatternValues = ( + panel: PanelSchema, + defaultIndex?: PanelSchema['default_index_pattern'] +) => { + const patterns: IndexPatternValue[] = []; + + if (panel.index_pattern) { + patterns.push(panel.index_pattern); + } + + panel.series.forEach((series) => { + const indexPattern = series.series_index_pattern; + if (indexPattern && series.override_index_pattern) { + patterns.push(indexPattern); + } + }); + + if (panel.annotations) { + panel.annotations.forEach((item) => { + const indexPattern = item.index_pattern; + if (indexPattern) { + patterns.push(indexPattern); + } + }); + } + + if (patterns.length === 0 && defaultIndex) { + patterns.push(defaultIndex); + } + + return uniq(patterns).sort(); +}; + +export const fetchIndexPattern = async ( + indexPatternValue: IndexPatternValue | undefined, + indexPatternsService: Pick +): Promise => { + let indexPattern: FetchedIndexPattern['indexPattern']; + let indexPatternString: string = ''; + + if (!indexPatternValue) { + indexPattern = await indexPatternsService.getDefault(); + } else { + if (isStringTypeIndexPattern(indexPatternValue)) { + indexPattern = (await indexPatternsService.find(indexPatternValue)).find( + (index) => index.title === indexPatternValue + ); + + if (!indexPattern) { + indexPatternString = indexPatternValue; + } + } else if (indexPatternValue.id) { + indexPattern = await indexPatternsService.get(indexPatternValue.id); + } + } + + return { + indexPattern, + indexPatternString: indexPattern?.title ?? indexPatternString, + }; +}; diff --git a/src/plugins/vis_type_timeseries/common/interval_regexp.js b/src/plugins/vis_type_timeseries/common/interval_regexp.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/interval_regexp.js rename to src/plugins/vis_type_timeseries/common/interval_regexp.ts diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index 7d93232f310c9..4aa69be346608 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -13,10 +13,12 @@ import { seriesItems, visPayloadSchema, fieldObject, + indexPattern, annotationsItems, } from './vis_schema'; import { PANEL_TYPES } from './panel_types'; import { TimeseriesUIRestrictions } from './ui_restrictions'; +import { IndexPattern } from '../../data/common'; export type AnnotationItemsSchema = TypeOf; export type SeriesItemsSchema = TypeOf; @@ -24,6 +26,38 @@ export type MetricsItemsSchema = TypeOf; export type PanelSchema = TypeOf; export type VisPayload = TypeOf; export type FieldObject = TypeOf; +export type IndexPatternValue = TypeOf; + +export interface FetchedIndexPattern { + indexPattern: IndexPattern | undefined | null; + indexPatternString: string | undefined; +} + +export type TimeseriesVisData = SeriesData | TableData; + +interface TableData { + type: PANEL_TYPES.TABLE; + uiRestrictions: TimeseriesUIRestrictions; + series?: PanelData[]; + pivot_label?: string; +} + +// series data is not fully typed yet +export type SeriesData = { + type: Exclude; + uiRestrictions: TimeseriesUIRestrictions; +} & { + [key: string]: PanelSeries; +}; + +interface PanelSeries { + annotations: { + [key: string]: unknown[]; + }; + id: string; + series: PanelData[]; + error?: unknown; +} export interface PanelData { id: string; @@ -31,26 +65,11 @@ export interface PanelData { data: Array<[number, number]>; } -// series data is not fully typed yet -interface SeriesData { - [key: string]: { - annotations: { - [key: string]: unknown[]; - }; - id: string; - series: PanelData[]; - error?: unknown; - }; -} +export const isVisTableData = (data: TimeseriesVisData): data is TableData => + data.type === PANEL_TYPES.TABLE; -export type TimeseriesVisData = SeriesData & { - type: PANEL_TYPES; - uiRestrictions: TimeseriesUIRestrictions; - /** - * series array is responsible only for "table" vis type - */ - series?: unknown[]; -}; +export const isVisSeriesData = (data: TimeseriesVisData): data is SeriesData => + data.type !== PANEL_TYPES.TABLE; export interface SanitizedFieldType { name: string; diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index a6bf70948bc1b..383b089593565 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -28,7 +28,7 @@ const numberOptional = schema.maybe(schema.number()); const queryObject = schema.object({ language: schema.string(), - query: schema.string(), + query: schema.oneOf([schema.string(), schema.any()]), }); const stringOrNumberOptionalNullable = schema.nullable( schema.oneOf([stringOptionalNullable, numberOptional]) @@ -37,6 +37,13 @@ const numberOptionalOrEmptyString = schema.maybe( schema.oneOf([numberOptional, schema.literal('')]) ); +export const indexPattern = schema.oneOf([ + schema.maybe(schema.string()), + schema.object({ + id: schema.string(), + }), +]); + export const fieldObject = stringOptionalNullable; export const annotationsItems = schema.object({ @@ -47,7 +54,7 @@ export const annotationsItems = schema.object({ id: schema.string(), ignore_global_filters: numberIntegerOptional, ignore_panel_filters: numberIntegerOptional, - index_pattern: stringOptionalNullable, + index_pattern: indexPattern, query_string: schema.maybe(queryObject), template: stringOptionalNullable, time_field: fieldObject, @@ -68,6 +75,7 @@ const gaugeColorRulesItems = schema.object({ operator: stringOptionalNullable, value: schema.maybe(schema.nullable(schema.number())), }); + export const metricsItems = schema.object({ field: fieldObject, id: stringRequired, @@ -167,7 +175,7 @@ export const seriesItems = schema.object({ point_size: numberOptionalOrEmptyString, separate_axis: numberIntegerOptional, seperate_axis: numberIntegerOptional, - series_index_pattern: stringOptionalNullable, + series_index_pattern: indexPattern, series_max_bars: numberIntegerOptional, series_time_field: fieldObject, series_interval: stringOptionalNullable, @@ -195,6 +203,7 @@ export const seriesItems = schema.object({ }); export const panel = schema.object({ + use_kibana_indexes: schema.maybe(schema.boolean()), annotations: schema.maybe(schema.arrayOf(annotationsItems)), axis_formatter: stringRequired, axis_position: stringRequired, @@ -215,10 +224,11 @@ export const panel = schema.object({ gauge_inner_width: stringOrNumberOptionalNullable, gauge_style: stringOptionalNullable, gauge_max: numberOptionalOrEmptyString, + hide_last_value_indicator: schema.boolean(), id: stringRequired, ignore_global_filters: numberOptional, ignore_global_filter: numberOptional, - index_pattern: stringRequired, + index_pattern: indexPattern, max_bars: numberIntegerOptional, interval: stringRequired, isModelInvalid: schema.maybe(schema.boolean()), diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index aa5eac84663ad..242b62a2c5ee4 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "visualize"], + "optionalPlugins": ["usageCollection"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx index 4fc7b89e23765..82989cc15d6c9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui'; import { METRIC_TYPES } from '../../../../common/metric_types'; - -import type { SanitizedFieldType } from '../../../../common/types'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; +import type { SanitizedFieldType, IndexPatternValue } from '../../../../common/types'; import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; // @ts-ignore @@ -20,7 +20,7 @@ import { isFieldEnabled } from '../../lib/check_ui_restrictions'; interface FieldSelectProps { type: string; fields: Record; - indexPattern: string; + indexPattern: IndexPatternValue; value?: string | null; onChange: (options: Array>) => void; disabled?: boolean; @@ -62,8 +62,10 @@ export function FieldSelect({ const selectedOptions: Array> = []; let newPlaceholder = placeholder; + const fieldsSelector = getIndexPatternKey(indexPattern); + const groupedOptions: EuiComboBoxProps['options'] = Object.values( - (fields[indexPattern] || []).reduce>>( + (fields[fieldsSelector] || []).reduce>>( (acc, field) => { if (placeholder === field?.name) { newPlaceholder = field.label ?? field.name; diff --git a/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js b/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js index f95eeb4816128..ab0db6daae18a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js @@ -32,8 +32,8 @@ import { EuiCode, EuiText, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPatternSelect } from './lib/index_pattern_select'; function newAnnotation() { return { @@ -91,7 +91,6 @@ export class AnnotationsEditor extends Component { const htmlId = htmlIdGenerator(model.id); const handleAdd = collectionActions.handleAdd.bind(null, this.props, newAnnotation); const handleDelete = collectionActions.handleDelete.bind(null, this.props, model); - const defaultIndexPattern = this.props.model.default_index_pattern; return (
@@ -108,30 +107,11 @@ export class AnnotationsEditor extends Component { - - } - helpText={ - defaultIndexPattern && - !model.index_pattern && - i18n.translate('visTypeTimeseries.annotationsEditor.searchByDefaultIndex', { - defaultMessage: 'Default index pattern is used. To query all indexes use *', - }) - } - fullWidth - > - - + { const isAutoOrGteInterval = isGteInterval(intervalValue) || isAutoInterval(intervalValue); @@ -64,7 +67,8 @@ export const IndexPattern = ({ onChange, disabled, model: _model, - allowLevelofDetail, + allowLevelOfDetail, + allowIndexSwitchingMode, }) => { const config = getUISettings(); const timeFieldName = `${prefix}time_field`; @@ -89,13 +93,6 @@ export const IndexPattern = ({ const handleTextChange = createTextHandler(onChange); const timeRangeOptions = [ - { - label: i18n.translate('visTypeTimeseries.indexPattern.timeRange.lastValue', { - defaultMessage: 'Last value', - }), - value: TIME_RANGE_DATA_MODES.LAST_VALUE, - disabled: !isTimerangeModeEnabled(TIME_RANGE_DATA_MODES.LAST_VALUE, uiRestrictions), - }, { label: i18n.translate('visTypeTimeseries.indexPattern.timeRange.entireTimeRange', { defaultMessage: 'Entire time range', @@ -103,6 +100,13 @@ export const IndexPattern = ({ value: TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, disabled: !isTimerangeModeEnabled(TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, uiRestrictions), }, + { + label: i18n.translate('visTypeTimeseries.indexPattern.timeRange.lastValue', { + defaultMessage: 'Last value', + }), + value: TIME_RANGE_DATA_MODES.LAST_VALUE, + disabled: !isTimerangeModeEnabled(TIME_RANGE_DATA_MODES.LAST_VALUE, uiRestrictions), + }, ]; const defaults = { @@ -127,6 +131,11 @@ export const IndexPattern = ({ updateControlValidity(intervalName, intervalValidation.isValid); }, [intervalName, intervalValidation.isValid, updateControlValidity]); + const toggleIndicatorDisplay = useCallback( + () => onChange({ [HIDE_LAST_VALUE_INDICATOR]: !model.hide_last_value_indicator }), + [model.hide_last_value_indicator, onChange] + ); + return (
{!isTimeSeries && ( @@ -139,6 +148,7 @@ export const IndexPattern = ({ })} > + ), + })} /> @@ -165,26 +183,13 @@ export const IndexPattern = ({ )} - - - + - {allowLevelofDetail && ( + {allowLevelOfDetail && ( { + if (!seriesData?.length) return {lastValueLabel}; + + const dateFormat = getUISettings().get('dateFormat'); + const scaledDataFormat = getUISettings().get('dateFormat:scaled'); + + const getFormattedPanelInterval = () => { + const interval = convertIntervalIntoUnit(panelInterval, false); + return interval && `${interval.unitValue}${interval.unitString}`; + }; + + const formatter = createIntervalBasedFormatter(panelInterval, scaledDataFormat, dateFormat); + const lastBucketDate = formatter(seriesData[seriesData.length - 1][0]); + const formattedPanelInterval = + (isAutoInterval(modelInterval) || isGteInterval(modelInterval)) && getFormattedPanelInterval(); + + const tooltipContent = ( + + + + + {formattedPanelInterval && ( + + + + )} + + ); + + return ( + + {}} + onClickAriaLabel={i18n.translate( + 'visTypeTimeseries.lastValueModeIndicator.lastValueModeBadgeAriaLabel', + { + defaultMessage: 'View last value details', + } + )} + > + {lastValueLabel} + + + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss new file mode 100644 index 0000000000000..eac3fa86a2567 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss @@ -0,0 +1,7 @@ +.tvbLastValueModePopover { + height: auto; +} + +.tvbLastValueModePopoverBody { + width: 360px; +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx new file mode 100644 index 0000000000000..4124adb6b93fa --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.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 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 './last_value_mode_popover.scss'; + +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiPopover, EuiPopoverTitle, EuiSwitch } from '@elastic/eui'; + +interface LastValueModePopoverProps { + isIndicatorDisplayed: boolean; + toggleIndicatorDisplay: () => void; +} + +export const LastValueModePopover = ({ + isIndicatorDisplayed, + toggleIndicatorDisplay, +}: LastValueModePopoverProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + return ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > +
+ + {i18n.translate('visTypeTimeseries.lastValueModePopover.title', { + defaultMessage: 'Last value options', + })} + + +
+
+ ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts similarity index 66% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts index e2eea5281b197..562aec31a0803 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts @@ -8,17 +8,21 @@ import moment from 'moment'; -function getFormat(interval, rules = []) { +function getFormat(interval: number, rules: string[][] = []) { for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; - if (!rule[0] || interval >= moment.duration(rule[0])) { + if (!rule[0] || interval >= Number(moment.duration(rule[0]))) { return rule[1]; } } } -export function createXaxisFormatter(interval, rules, dateFormat) { - return (val) => { +export function createIntervalBasedFormatter( + interval: number, + rules: string[][], + dateFormat: string +) { + return (val: moment.MomentInput): string => { return moment(val).format(getFormat(interval, rules) ?? dateFormat); }; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts similarity index 70% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts index dc297df39a870..9ff0d6832d0cd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts @@ -13,6 +13,8 @@ import { search } from '../../../../../../plugins/data/public'; const { parseEsInterval } = search.aggs; import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; import { AUTO_INTERVAL } from '../../../../common/constants'; +import { isVisTableData, PanelData, TimeseriesVisData } from '../../../../common/types'; +import { TimeseriesVisParams } from '../../../types'; export const unitLookup = { s: i18n.translate('visTypeTimeseries.getInterval.secondsLabel', { defaultMessage: 'seconds' }), @@ -24,9 +26,11 @@ export const unitLookup = { y: i18n.translate('visTypeTimeseries.getInterval.yearsLabel', { defaultMessage: 'years' }), }; -export const convertIntervalIntoUnit = (interval, hasTranslateUnitString = true) => { +type TimeUnit = keyof typeof unitLookup; + +export const convertIntervalIntoUnit = (interval: number, hasTranslateUnitString = true) => { // Iterate units from biggest to smallest - const units = Object.keys(unitLookup).reverse(); + const units = Object.keys(unitLookup).reverse() as TimeUnit[]; const duration = moment.duration(interval, 'ms'); for (let i = 0; i < units.length; i++) { @@ -41,11 +45,16 @@ export const convertIntervalIntoUnit = (interval, hasTranslateUnitString = true) } }; -export const isGteInterval = (interval) => GTE_INTERVAL_RE.test(interval); -export const isAutoInterval = (interval) => !interval || interval === AUTO_INTERVAL; +export const isGteInterval = (interval: string) => GTE_INTERVAL_RE.test(interval); +export const isAutoInterval = (interval: string) => !interval || interval === AUTO_INTERVAL; + +interface ValidationResult { + isValid: boolean; + errorMessage?: string; +} -export const validateReInterval = (intervalValue) => { - const validationResult = {}; +export const validateReInterval = (intervalValue: string) => { + const validationResult = {} as ValidationResult; try { parseEsInterval(intervalValue); @@ -58,14 +67,12 @@ export const validateReInterval = (intervalValue) => { return validationResult; }; -export const getInterval = (visData, model) => { - let series; - - if (model && model.type === 'table') { - series = get(visData, `series[0].series`, []); - } else { - series = get(visData, `${model.id}.series`, []); - } +export const getInterval = (visData: TimeseriesVisData, model: TimeseriesVisParams) => { + const series = get( + visData, + isVisTableData(visData) ? `series[0].series` : `${model.id}.series`, + [] + ) as PanelData[]; return series.reduce((currentInterval, item) => { if (item.data.length > 1) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx new file mode 100644 index 0000000000000..7111a63244c7f --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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, { useCallback, useState, useEffect } from 'react'; +import type { UnwrapPromise } from '@kbn/utility-types'; +import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; +import { getDataStart } from '../../../../services'; + +import { SwitchModePopover } from './switch_mode_popover'; + +import type { SelectIndexComponentProps } from './types'; +import type { IndexPatternValue } from '../../../../../common/types'; +import type { IndexPatternsService } from '../../../../../../data/public'; + +/** @internal **/ +type IdsWithTitle = UnwrapPromise>; + +/** @internal **/ +type SelectedOptions = EuiComboBoxProps['selectedOptions']; + +const toComboBoxOptions = (options: IdsWithTitle) => + options.map(({ title, id }) => ({ label: title, id })); + +export const ComboBoxSelect = ({ + fetchedIndex, + onIndexChange, + onModeChange, + disabled, + placeholder, + allowSwitchMode, + 'data-test-subj': dataTestSubj, +}: SelectIndexComponentProps) => { + const [availableIndexes, setAvailableIndexes] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + + const onComboBoxChange: EuiComboBoxProps['onChange'] = useCallback( + ([selected]) => { + onIndexChange(selected ? { id: selected.id } : ''); + }, + [onIndexChange] + ); + + useEffect(() => { + let options: SelectedOptions = []; + const { indexPattern, indexPatternString } = fetchedIndex; + + if (indexPattern || indexPatternString) { + if (!indexPattern) { + options = [{ label: indexPatternString ?? '' }]; + } else { + options = [ + { + id: indexPattern.id, + label: indexPattern.title, + }, + ]; + } + } + setSelectedOptions(options); + }, [fetchedIndex]); + + useEffect(() => { + async function fetchIndexes() { + setAvailableIndexes(await getDataStart().indexPatterns.getIdsWithTitle()); + } + + fetchIndexes(); + }, []); + + return ( + + ), + })} + /> + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx new file mode 100644 index 0000000000000..86d1758932301 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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, { useCallback, useState, useEffect } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; + +import { EuiFieldText, EuiFieldTextProps } from '@elastic/eui'; +import { SwitchModePopover } from './switch_mode_popover'; + +import type { SelectIndexComponentProps } from './types'; + +export const FieldTextSelect = ({ + fetchedIndex, + onIndexChange, + disabled, + placeholder, + onModeChange, + allowSwitchMode, + 'data-test-subj': dataTestSubj, +}: SelectIndexComponentProps) => { + const [inputValue, setInputValue] = useState(); + const { indexPatternString } = fetchedIndex; + + const onFieldTextChange: EuiFieldTextProps['onChange'] = useCallback((e) => { + setInputValue(e.target.value); + }, []); + + useEffect(() => { + if (inputValue === undefined) { + setInputValue(indexPatternString ?? ''); + } + }, [indexPatternString, inputValue]); + + useDebounce( + () => { + if (inputValue !== indexPatternString) { + onIndexChange(inputValue); + } + }, + 150, + [inputValue, onIndexChange] + ); + + return ( + + ), + })} + /> + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index.ts new file mode 100644 index 0000000000000..584f13e7a025b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { IndexPatternSelect } from './index_pattern_select'; 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 new file mode 100644 index 0000000000000..28b9c173a2b1b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 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, useContext, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiFormRow, EuiText, EuiLink, htmlIdGenerator } from '@elastic/eui'; +import { getCoreStart, getDataStart } from '../../../../services'; +import { PanelModelContext } from '../../../contexts/panel_model_context'; + +import { + isStringTypeIndexPattern, + fetchIndexPattern, +} from '../../../../../common/index_patterns_utils'; + +import { FieldTextSelect } from './field_text_select'; +import { ComboBoxSelect } from './combo_box_select'; + +import type { IndexPatternValue, FetchedIndexPattern } from '../../../../../common/types'; + +const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes'; + +interface IndexPatternSelectProps { + value: IndexPatternValue; + indexPatternName: string; + onChange: Function; + disabled?: boolean; + allowIndexSwitchingMode?: boolean; +} + +const defaultIndexPatternHelpText = i18n.translate( + 'visTypeTimeseries.indexPatternSelect.defaultIndexPatternText', + { + defaultMessage: 'Default index pattern is used.', + } +); + +const queryAllIndexesHelpText = i18n.translate( + 'visTypeTimeseries.indexPatternSelect.queryAllIndexesText', + { + defaultMessage: 'To query all indexes use *', + } +); + +const indexPatternLabel = i18n.translate('visTypeTimeseries.indexPatternSelect.label', { + defaultMessage: 'Index pattern', +}); + +export const IndexPatternSelect = ({ + value, + indexPatternName, + onChange, + disabled, + allowIndexSwitchingMode, +}: IndexPatternSelectProps) => { + const htmlId = htmlIdGenerator(); + const panelModel = useContext(PanelModelContext); + const [fetchedIndex, setFetchedIndex] = useState(); + const useKibanaIndices = Boolean(panelModel?.[USE_KIBANA_INDEXES_KEY]); + const Component = useKibanaIndices ? ComboBoxSelect : FieldTextSelect; + + const onIndexChange = useCallback( + (index: IndexPatternValue) => { + onChange({ + [indexPatternName]: index, + }); + }, + [indexPatternName, onChange] + ); + + const onModeChange = useCallback( + (useKibanaIndexes: boolean, index?: FetchedIndexPattern) => { + onChange({ + [USE_KIBANA_INDEXES_KEY]: useKibanaIndexes, + [indexPatternName]: index?.indexPattern?.id + ? { + id: index.indexPattern.id, + } + : '', + }); + }, + [onChange, indexPatternName] + ); + + const navigateToCreateIndexPatternPage = useCallback(() => { + const coreStart = getCoreStart(); + + coreStart.application.navigateToApp('management', { + path: `/kibana/indexPatterns/create?name=${fetchedIndex!.indexPatternString ?? ''}`, + }); + }, [fetchedIndex]); + + useEffect(() => { + async function fetchIndex() { + const { indexPatterns } = getDataStart(); + + setFetchedIndex( + value + ? await fetchIndexPattern(value, indexPatterns) + : { + indexPattern: undefined, + indexPatternString: undefined, + } + ); + } + + fetchIndex(); + }, [value]); + + if (!fetchedIndex) { + return null; + } + + return ( + + + + + + ) : null + } + > + + + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx new file mode 100644 index 0000000000000..5f5506ce4a332 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonIcon, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiSwitch, + EuiText, +} from '@elastic/eui'; + +import type { PopoverProps } from './types'; + +export const SwitchModePopover = ({ onModeChange, useKibanaIndices }: PopoverProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []); + + const switchMode = useCallback(() => { + onModeChange(!useKibanaIndices); + }, [onModeChange, useKibanaIndices]); + + return ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + style={{ height: 'auto' }} + > +
+ + {i18n.translate('visTypeTimeseries.indexPatternSelect.switchModePopover.title', { + defaultMessage: 'Index pattern selection mode', + })} + + + + + + +
+
+ ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts new file mode 100644 index 0000000000000..93b15402e3c24 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.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 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 { Assign } from '@kbn/utility-types'; +import type { FetchedIndexPattern, IndexPatternValue } from '../../../../../common/types'; + +/** @internal **/ +export interface SelectIndexComponentProps { + fetchedIndex: FetchedIndexPattern; + onIndexChange: (value: IndexPatternValue) => void; + onModeChange: (useKibanaIndexes: boolean, index?: FetchedIndexPattern) => void; + 'data-test-subj': string; + placeholder?: string; + disabled?: boolean; + allowSwitchMode?: boolean; +} + +/** @internal **/ +export type PopoverProps = Assign< + Pick, + { + useKibanaIndices: boolean; + } +>; 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 e302bbb9adb0b..f39ff6923f5ce 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 @@ -29,12 +29,11 @@ import type { Writable } from '@kbn/utility-types'; // @ts-ignore import { SeriesEditor } from '../series_editor'; -// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts +// @ts-expect-error not typed yet import { IndexPattern } from '../index_pattern'; import { createSelectHandler } from '../lib/create_select_handler'; import { ColorRules } from '../color_rules'; import { ColorPicker } from '../color_picker'; -// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; import { YesNo } from '../yes_no'; @@ -128,6 +127,7 @@ export class GaugePanelConfig extends Component< fields={this.props.fields} model={this.props.model} onChange={this.props.onChange} + allowIndexSwitchingMode={true} /> @@ -149,10 +149,10 @@ export class GaugePanelConfig extends Component< language: model.filter?.language || getDefaultQueryLanguage(), query: model.filter?.query || '', }} - onChange={(filter: PanelConfigProps['model']['filter']) => - this.props.onChange({ filter }) - } - indexPatterns={[model.index_pattern || model.default_index_pattern]} + onChange={(filter) => { + this.props.onChange({ filter }); + }} + indexPatterns={[model.index_pattern || model.default_index_pattern || '']} />
@@ -321,6 +321,7 @@ export class GaugePanelConfig extends Component< this.switchTab(PANEL_CONFIG_TABS.DATA)} + data-test-subj="gaugeEditorDataBtn" > this.switchTab(PANEL_CONFIG_TABS.OPTIONS)} + data-test-subj="gaugeEditorPanelOptionsBtn" > @@ -161,13 +161,13 @@ export class MarkdownPanelConfig extends Component< > { + this.props.onChange({ filter }); }} - onChange={(filter: PanelConfigProps['model']['filter']) => - this.props.onChange({ filter }) - } - indexPatterns={[model.index_pattern || model.default_index_pattern]} + indexPatterns={[model.index_pattern || model.default_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 ec11f94d245a0..3ab49c1bef873 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 @@ -25,12 +25,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; // @ts-expect-error import { SeriesEditor } from '../series_editor'; -// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts +// @ts-expect-error not typed yet import { IndexPattern } from '../index_pattern'; import { ColorRules } from '../color_rules'; import { YesNo } from '../yes_no'; - -// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; import { limitOfSeries } from '../../../../common/ui_restrictions'; @@ -93,6 +91,7 @@ export class MetricPanelConfig extends Component< fields={this.props.fields} model={this.props.model} onChange={this.props.onChange} + allowIndexSwitchingMode={true} /> @@ -111,13 +110,13 @@ export class MetricPanelConfig extends Component< > { + this.props.onChange({ filter }); }} - onChange={(filter: PanelConfigProps['model']['filter']) => - this.props.onChange({ filter }) - } - indexPatterns={[model.index_pattern || model.default_index_pattern]} + indexPatterns={[model.index_pattern || model.default_index_pattern || '']} /> @@ -166,6 +165,7 @@ export class MetricPanelConfig extends Component< this.switchTab(PANEL_CONFIG_TABS.DATA)} + data-test-subj="metricEditorDataBtn" > - -
- -
-
+ + +
+ +
+
+
); } 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 20e07be4e3fa4..f3d01df19666a 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 @@ -31,16 +31,17 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { FieldSelect } from '../aggs/field_select'; // @ts-expect-error not typed yet import { SeriesEditor } from '../series_editor'; -// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts +// @ts-expect-error not typed yet import { IndexPattern } from '../index_pattern'; import { YesNo } from '../yes_no'; -// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging + import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; import { VisDataContext } from '../../contexts/vis_data_context'; import { BUCKET_TYPES } from '../../../../common/metric_types'; import { PanelConfigProps, PANEL_CONFIG_TABS } from './types'; import { TimeseriesVisParams } from '../../../types'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export class TablePanelConfig extends Component< PanelConfigProps, @@ -66,7 +67,7 @@ export class TablePanelConfig extends Component< handlePivotChange = (selectedOption: Array>) => { const { fields, model } = this.props; const pivotId = get(selectedOption, '[0].value', null); - const field = fields[model.index_pattern].find((f) => f.name === pivotId); + const field = fields[getIndexPatternKey(model.index_pattern)].find((f) => f.name === pivotId); const pivotType = get(field, 'type', model.pivot_type); this.props.onChange({ @@ -237,15 +238,13 @@ export class TablePanelConfig extends Component< > - this.props.onChange({ filter }) - } - indexPatterns={[model.index_pattern || model.default_index_pattern]} + onChange={(filter) => { + this.props.onChange({ filter }); + }} + indexPatterns={[model.index_pattern || model.default_index_pattern || '']} /> @@ -274,6 +273,7 @@ export class TablePanelConfig extends Component< this.switchTab(PANEL_CONFIG_TABS.DATA)} + data-test-subj="tableEditorDataBtn" > this.switchTab(PANEL_CONFIG_TABS.OPTIONS)} + data-test-subj="tableEditorPanelOptionsBtn" > - @@ -202,13 +201,13 @@ export class TimeseriesPanelConfig extends Component< > { + this.props.onChange({ filter }); }} - onChange={(filter: PanelConfigProps['model']['filter']) => - this.props.onChange({ filter }) - } - indexPatterns={[model.index_pattern || model.default_index_pattern]} + indexPatterns={[model.index_pattern || model.default_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 184063f88ef03..78ac11eb39744 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 @@ -33,7 +33,6 @@ import { ColorRules } from '../color_rules'; import { ColorPicker } from '../color_picker'; import { YesNo } from '../yes_no'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; -// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging import { QueryBarWrapper } from '../query_bar_wrapper'; import { PanelConfigProps, PANEL_CONFIG_TABS } from './types'; import { TimeseriesVisParams } from '../../../types'; @@ -120,6 +119,7 @@ export class TopNPanelConfig extends Component< fields={this.props.fields} model={this.props.model} onChange={this.props.onChange} + allowIndexSwitchingMode={true} /> @@ -138,13 +138,13 @@ export class TopNPanelConfig extends Component< > - this.props.onChange({ filter }) - } - indexPatterns={[model.index_pattern || model.default_index_pattern]} + onChange={(filter: PanelConfigProps['model']['filter']) => { + this.props.onChange({ filter }); + }} + indexPatterns={[model.index_pattern || model.default_index_pattern || '']} /> @@ -225,6 +225,7 @@ export class TopNPanelConfig extends Component< this.switchTab(PANEL_CONFIG_TABS.DATA)} + data-test-subj="topNEditorDataBtn" > this.switchTab(PANEL_CONFIG_TABS.OPTIONS)} + data-test-subj="topNEditorPanelOptionsBtn" > & { + indexPatterns: IndexPatternValue[]; +}; + +export function QueryBarWrapper({ query, onChange, indexPatterns }: QueryBarWrapperProps) { + const { indexPatterns: indexPatternsService } = getDataStart(); + const [indexes, setIndexes] = useState([]); + + const coreStartContext = useContext(CoreStartContext); + + 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); + } + } + } + setIndexes(i); + } + + fetchIndexes(); + }, [indexPatterns, indexPatternsService]); + + return ( + + ); +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js index 4e48ed4406ea5..3185503acb569 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config.js @@ -137,5 +137,5 @@ SeriesConfig.propTypes = { panel: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js index 0b67d52c23cd2..950101103b3a5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js @@ -90,5 +90,5 @@ SeriesConfigQueryBarWithIgnoreGlobalFilter.propTypes = { onChange: PropTypes.func, model: PropTypes.object, panel: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; 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 5891320aa684f..b996abd6373ab 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 @@ -25,7 +25,7 @@ import { EuiFieldText, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { FIELD_TYPES } from '../../../../common/field_types'; +import { KBN_FIELD_TYPES } from '../../../../../data/public'; import { STACKED_OPTIONS } from '../../visualizations/constants'; const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' }; @@ -133,7 +133,7 @@ export const SplitByTermsUI = ({ - {selectedFieldType === FIELD_TYPES.STRING && ( + {selectedFieldType === KBN_FIELD_TYPES.STRING && ( @@ -87,18 +97,40 @@ function TimeseriesVisualization({ const VisComponent = TimeseriesVisTypes[model.type]; + const isLastValueMode = + !model.time_range_mode || model.time_range_mode === TIME_RANGE_DATA_MODES.LAST_VALUE; + const shouldDisplayLastValueIndicator = + isLastValueMode && !model.hide_last_value_indicator && model.type !== PANEL_TYPES.TIMESERIES; + if (VisComponent) { return ( - + + {shouldDisplayLastValueIndicator && ( + + + + )} + + + + ); } 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 ffef437358c3d..887075e9e4e48 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 @@ -15,18 +15,19 @@ import { EventEmitter } from 'events'; import { IUiSettingsClient } from 'kibana/public'; import { TimeRange } from 'src/plugins/data/public'; import { - PersistedState, Vis, + PersistedState, VisualizeEmbeddableContract, } from 'src/plugins/visualizations/public'; -import { TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; +import { IndexPatternValue, TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; // @ts-expect-error import { VisEditorVisualization } from './vis_editor_visualization'; import { PanelConfig } from './panel_config'; -import { extractIndexPatterns } from '../../../common/extract_index_patterns'; +import { extractIndexPatternValues } from '../../../common/index_patterns_utils'; +import { TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/timerange_data_modes'; import { VisPicker } from './vis_picker'; import { fetchFields, VisFields } from '../lib/fetch_fields'; import { getDataStart, getCoreStart } from '../../services'; @@ -47,7 +48,7 @@ export interface TimeseriesEditorProps { interface TimeseriesEditorState { autoApply: boolean; dirty: boolean; - extractedIndexPatterns: string[]; + extractedIndexPatterns: IndexPatternValue[]; model: TimeseriesVisParams; visFields?: VisFields; } @@ -64,7 +65,17 @@ export class VisEditor extends Component { + abortableFetchFields = (extractedIndexPatterns: IndexPatternValue[]) => { this.abortControllerFetchFields?.abort(); this.abortControllerFetchFields = new AbortController(); @@ -202,7 +213,7 @@ export class VisEditor extends Component { const defaultIndexTitle = index?.title ?? ''; - const indexPatterns = extractIndexPatterns(this.props.vis.params, defaultIndexTitle); + const indexPatterns = extractIndexPatternValues(this.props.vis.params, defaultIndexTitle); const visFields = await fetchFields(indexPatterns); this.setState((state) => ({ diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js index bfce125a7ed87..bb264aaacbfbf 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js @@ -8,17 +8,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { get } from 'lodash'; import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { - getInterval, - convertIntervalIntoUnit, - isAutoInterval, - isGteInterval, -} from './lib/get_interval'; -import { AUTO_INTERVAL } from '../../../common/constants'; -import { PANEL_TYPES } from '../../../common/panel_types'; const MIN_CHART_HEIGHT = 300; @@ -28,7 +19,6 @@ class VisEditorVisualizationUI extends Component { this.state = { height: MIN_CHART_HEIGHT, dragging: false, - panelInterval: 0, }; this._visEl = React.createRef(); @@ -65,18 +55,7 @@ class VisEditorVisualizationUI extends Component { await this._handler.render(this._visEl.current); this.props.eventEmitter.emit('embeddableRendered'); - this._subscription = this._handler.handler.data$.subscribe((data) => { - this.setPanelInterval(data.value.visData); - onDataChange(data.value); - }); - } - - setPanelInterval(visData) { - const panelInterval = getInterval(visData, this.props.model); - - if (this.state.panelInterval !== panelInterval) { - this.setState({ panelInterval }); - } + this._subscription = this._handler.handler.data$.subscribe((data) => onDataChange(data.value)); } /** @@ -98,28 +77,6 @@ class VisEditorVisualizationUI extends Component { } }; - hasShowPanelIntervalValue() { - const type = get(this.props, 'model.type', ''); - const interval = get(this.props, 'model.interval', AUTO_INTERVAL); - - return ( - [ - PANEL_TYPES.METRIC, - PANEL_TYPES.TOP_N, - PANEL_TYPES.GAUGE, - PANEL_TYPES.MARKDOWN, - PANEL_TYPES.TABLE, - ].includes(type) && - (isAutoInterval(interval) || isGteInterval(interval)) - ); - } - - getFormattedPanelInterval() { - const interval = convertIntervalIntoUnit(this.state.panelInterval, false); - - return interval ? `${interval.unitValue}${interval.unitString}` : null; - } - componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouseMove); window.removeEventListener('mouseup', this.handleMouseUp); @@ -154,8 +111,6 @@ class VisEditorVisualizationUI extends Component { style.userSelect = 'none'; } - const panelInterval = this.hasShowPanelIntervalValue() && this.getFormattedPanelInterval(); - let applyMessage = ( - {panelInterval && ( - - -

- -

-
-
- )} -

{applyMessage}

diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js index 2909167031d08..46cc8b6ebe635 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js @@ -198,7 +198,7 @@ GaugeSeriesUi.propTypes = { visible: PropTypes.bool, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; export const GaugeSeries = injectI18n(GaugeSeriesUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js index 6f00abe5aa2c0..f9817242a101a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js @@ -200,7 +200,7 @@ MarkdownSeriesUi.propTypes = { visible: PropTypes.bool, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; export const MarkdownSeries = injectI18n(MarkdownSeriesUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js index 64425cf534226..5ec2378792812 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js @@ -211,7 +211,7 @@ MetricSeriesUi.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; export const MetricSeries = injectI18n(MetricSeriesUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js index fecd6cde1dca8..0ba8d3e855365 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js @@ -9,6 +9,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; + import { DataFormatPicker } from '../../data_format_picker'; import { createSelectHandler } from '../../lib/create_select_handler'; import { createTextHandler } from '../../lib/create_text_handler'; @@ -28,11 +30,11 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; - import { QueryBarWrapper } from '../../query_bar_wrapper'; -class TableSeriesConfigUI extends Component { + +export class TableSeriesConfig extends Component { UNSAFE_componentWillMount() { const { model } = this.props; if (!model.color_rules || (model.color_rules && model.color_rules.length === 0)) { @@ -48,68 +50,58 @@ class TableSeriesConfigUI extends Component { const handleSelectChange = createSelectHandler(this.props.onChange); const handleTextChange = createTextHandler(this.props.onChange); const htmlId = htmlIdGenerator(); - const { intl } = this.props; const functionOptions = [ { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.sumLabel', + label: i18n.translate('visTypeTimeseries.table.sumLabel', { defaultMessage: 'Sum', }), value: 'sum', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.maxLabel', + label: i18n.translate('visTypeTimeseries.table.maxLabel', { defaultMessage: 'Max', }), value: 'max', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.minLabel', + label: i18n.translate('visTypeTimeseries.table.minLabel', { defaultMessage: 'Min', }), value: 'min', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.avgLabel', + label: i18n.translate('visTypeTimeseries.table.avgLabel', { defaultMessage: 'Avg', }), value: 'mean', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.overallSumLabel', + label: i18n.translate('visTypeTimeseries.table.overallSumLabel', { defaultMessage: 'Overall Sum', }), value: 'overall_sum', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.overallMaxLabel', + label: i18n.translate('visTypeTimeseries.table.overallMaxLabel', { defaultMessage: 'Overall Max', }), value: 'overall_max', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.overallMinLabel', + label: i18n.translate('visTypeTimeseries.table.overallMinLabel', { defaultMessage: 'Overall Min', }), value: 'overall_min', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.overallAvgLabel', + label: i18n.translate('visTypeTimeseries.table.overallAvgLabel', { defaultMessage: 'Overall Avg', }), value: 'overall_avg', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.table.cumulativeSumLabel', + label: i18n.translate('visTypeTimeseries.table.cumulativeSumLabel', { defaultMessage: 'Cumulative Sum', }), value: 'cumulative_sum', @@ -170,11 +162,8 @@ class TableSeriesConfigUI extends Component { > this.props.onChange({ filter })} indexPatterns={[this.props.indexPatternForQuery]} @@ -259,11 +248,9 @@ class TableSeriesConfigUI extends Component { } } -TableSeriesConfigUI.propTypes = { +TableSeriesConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; - -export const TableSeriesConfig = injectI18n(TableSeriesConfigUI); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js index a56afd1f817b3..acd2f4cc17d4a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js @@ -186,7 +186,7 @@ TableSeriesUI.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; export const TableSeries = injectI18n(TableSeriesUI); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js index 3df12dafd5a66..22bf2fa4ca708 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -542,7 +542,7 @@ export const TimeseriesConfig = injectI18n(function (props) { {...props} prefix="series_" disabled={!model.override_index_pattern} - allowLevelofDetail={true} + allowLevelOfDetail={true} />
@@ -555,6 +555,6 @@ TimeseriesConfig.propTypes = { model: PropTypes.object, panel: PropTypes.object, onChange: PropTypes.func, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), seriesQuantity: PropTypes.object, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js index 76df07ce7c8c4..bb10ac57c5ae9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js @@ -209,7 +209,7 @@ TimeseriesSeriesUI.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), seriesQuantity: PropTypes.object, }; 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 5a2fc05817f71..ae3fa4d9dcca4 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 @@ -19,7 +19,7 @@ import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public import { replaceVars } from '../../lib/replace_vars'; import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; -import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; +import { createIntervalBasedFormatter } from '../../lib/create_interval_based_formatter'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; import { getCoreStart } from '../../../../services'; @@ -35,7 +35,11 @@ class TimeseriesVisualization extends Component { dateFormat = this.props.getConfig('dateFormat'); xAxisFormatter = (interval) => (val) => { - const formatter = createXaxisFormatter(interval, this.scaledDataFormat, this.dateFormat); + const formatter = createIntervalBasedFormatter( + interval, + this.scaledDataFormat, + this.dateFormat + ); return formatter(val); }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js index bfe446a8226e8..61bb7e2473dd9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js @@ -200,5 +200,5 @@ TopNSeries.propTypes = { togglePanelActivation: PropTypes.func, uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, - indexPatternForQuery: PropTypes.string, + indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/panel_model_context.ts b/src/plugins/vis_type_timeseries/public/application/contexts/panel_model_context.ts new file mode 100644 index 0000000000000..534f686ca13fc --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/contexts/panel_model_context.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { PanelSchema } from '../../../common/types'; + +export const PanelModelContext = React.createContext(null); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts index 088930f90a765..af3ddd643cac8 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts +++ b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts @@ -9,12 +9,14 @@ import { i18n } from '@kbn/i18n'; import { getCoreStart, getDataStart } from '../../services'; import { ROUTES } from '../../../common/constants'; -import { SanitizedFieldType } from '../../../common/types'; +import { SanitizedFieldType, IndexPatternValue } from '../../../common/types'; +import { getIndexPatternKey } from '../../../common/index_patterns_utils'; +import { toSanitizedFieldType } from '../../../common/fields_utils'; export type VisFields = Record; export async function fetchFields( - indexes: string[] = [], + indexes: IndexPatternValue[] = [], signal?: AbortSignal ): Promise { const patterns = Array.isArray(indexes) ? indexes : [indexes]; @@ -25,26 +27,33 @@ export async function fetchFields( const defaultIndexPattern = await dataStart.indexPatterns.getDefault(); const indexFields = await Promise.all( patterns.map(async (pattern) => { - return coreStart.http.get(ROUTES.FIELDS, { - query: { - index: pattern, - }, - signal, - }); + if (typeof pattern !== 'string' && pattern?.id) { + return toSanitizedFieldType( + (await dataStart.indexPatterns.get(pattern.id)).getNonScriptedFields() + ); + } else { + return coreStart.http.get(ROUTES.FIELDS, { + query: { + index: `${pattern ?? ''}`, + }, + signal, + }); + } }) ); const fields: VisFields = patterns.reduce( (cumulatedFields, currentPattern, index) => ({ ...cumulatedFields, - [currentPattern]: indexFields[index], + [getIndexPatternKey(currentPattern)]: indexFields[index], }), {} ); - if (defaultIndexPattern?.title && patterns.includes(defaultIndexPattern.title)) { - fields[''] = fields[defaultIndexPattern.title]; + if (defaultIndexPattern) { + fields[''] = toSanitizedFieldType(await defaultIndexPattern.getNonScriptedFields()); } + return fields; } catch (error) { if (error.name !== 'AbortError') { diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 9e996fcc74833..5d5e082b2b7bb 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { TSVB_EDITOR_NAME } from './application'; import { PANEL_TYPES } from '../common/panel_types'; +import { isStringTypeIndexPattern } from '../common/index_patterns_utils'; import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public'; import { getDataStart } from './services'; @@ -53,6 +54,7 @@ export const metricsVisDefinition = { ], time_field: '', index_pattern: '', + use_kibana_indexes: true, interval: '', axis_position: 'left', axis_formatter: 'number', @@ -77,7 +79,20 @@ export const metricsVisDefinition = { inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { const { indexPatterns } = getDataStart(); + const indexPatternValue = params.index_pattern; - return params.index_pattern ? await indexPatterns.find(params.index_pattern) : []; + if (indexPatternValue) { + if (isStringTypeIndexPattern(indexPatternValue)) { + return await indexPatterns.find(indexPatternValue); + } + + if (indexPatternValue.id) { + return [await indexPatterns.get(indexPatternValue.id)]; + } + } + + const defaultIndex = await indexPatterns.getDefault(); + + return defaultIndex ? [defaultIndex] : []; }, }; diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx index c314594aa5420..208d1b3325e3d 100644 --- a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx @@ -9,12 +9,13 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; import type { PersistedState } from '../../visualizations/public'; import { VisualizationContainer } from '../../visualizations/public'; import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; import { TimeseriesRenderValue } from './metrics_fn'; -import { TimeseriesVisData } from '../common/types'; +import { isVisTableData, TimeseriesVisData } from '../common/types'; import { TimeseriesVisParams } from './types'; import { getChartsSetup } from './services'; @@ -24,7 +25,7 @@ const TimeseriesVisualization = lazy( const checkIfDataExists = (visData: TimeseriesVisData | {}, model: TimeseriesVisParams) => { if ('type' in visData) { - const data = visData.type === 'table' ? visData.series : visData?.[model.id]?.series; + const data = isVisTableData(visData) ? visData.series : visData?.[model.id]?.series; return Boolean(data?.length); } @@ -46,22 +47,24 @@ export const getTimeseriesVisRenderer: (deps: { const palettesService = await palettes.getPalettes(); render( - - + - , + showNoResult={showNoResult} + > + + + , domNode ); }, diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index 37eda1b1338d4..f41a6374462dc 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -15,9 +15,13 @@ export { VisTypeTimeseriesSetup } from './plugin'; export const config: PluginConfigDescriptor = { deprecations: ({ unused, renameFromRoot }) => [ // In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries': - renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', true), - renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', true), - renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', true), + renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', { silent: true }), + renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', { + silent: true, + }), + renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', { + silent: true, + }), // Unused properties which should be removed after releasing Kibana v8.0: unused('chartResolution'), @@ -29,3 +33,5 @@ export const config: PluginConfigDescriptor = { export function plugin(initializerContext: PluginInitializerContext) { return new VisTypeTimeseriesPlugin(initializerContext); } + +export { TimeseriesVisData, isVisSeriesData, isVisTableData } from '../common/types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index f1bc5a11550e9..b0e85f8e44fbe 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -10,6 +10,7 @@ import { uniqBy } from 'lodash'; import { Framework } from '../plugin'; import { VisTypeTimeseriesFieldsRequest, VisTypeTimeseriesRequestHandlerContext } from '../types'; +import { getCachedIndexPatternFetcher } from './search_strategies/lib/cached_index_pattern_fetcher'; export async function getFields( requestContext: VisTypeTimeseriesRequestHandlerContext, @@ -17,26 +18,29 @@ export async function getFields( framework: Framework, indexPatternString: string ) { + const indexPatternsService = await framework.getIndexPatternsService(requestContext); + const cachedIndexPatternFetcher = getCachedIndexPatternFetcher(indexPatternsService); + if (!indexPatternString) { - const indexPatternsService = await framework.getIndexPatternsService(requestContext); const defaultIndexPattern = await indexPatternsService.getDefault(); indexPatternString = defaultIndexPattern?.title ?? ''; } + const fetchedIndex = await cachedIndexPatternFetcher(indexPatternString); + const { searchStrategy, capabilities, } = (await framework.searchStrategyRegistry.getViableStrategy( requestContext, request, - indexPatternString + fetchedIndex ))!; const fields = await searchStrategy.getFieldsForWildcard( - requestContext, - request, - indexPatternString, + fetchedIndex, + indexPatternsService, capabilities ); diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index 0ad50a296b481..d91104fb299d7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -19,6 +19,7 @@ import type { import { getSeriesData } from './vis_data/get_series_data'; import { getTableData } from './vis_data/get_table_data'; import { getEsQueryConfig } from './vis_data/helpers/get_es_query_uisettings'; +import { getCachedIndexPatternFetcher } from './search_strategies/lib/cached_index_pattern_fetcher'; export async function getVisData( requestContext: VisTypeTimeseriesRequestHandlerContext, @@ -29,12 +30,14 @@ export async function getVisData( const esShardTimeout = await framework.getEsShardTimeout(); const indexPatternsService = await framework.getIndexPatternsService(requestContext); const esQueryConfig = await getEsQueryConfig(uiSettings); + const services: VisTypeTimeseriesRequestServices = { esQueryConfig, esShardTimeout, indexPatternsService, uiSettings, searchStrategyRegistry: framework.searchStrategyRegistry, + cachedIndexPatternFetcher: getCachedIndexPatternFetcher(indexPatternsService), }; const promises = request.body.panels.map((panel) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts new file mode 100644 index 0000000000000..aeaf3ca2cd327 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.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 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 { IndexPattern, IndexPatternsService } from 'src/plugins/data/server'; +import { + getCachedIndexPatternFetcher, + CachedIndexPatternFetcher, +} from './cached_index_pattern_fetcher'; + +describe('CachedIndexPatternFetcher', () => { + let mockedIndices: IndexPattern[] | []; + let cachedIndexPatternFetcher: CachedIndexPatternFetcher; + + beforeEach(() => { + mockedIndices = []; + + const indexPatternsService = ({ + getDefault: jest.fn(() => Promise.resolve({ id: 'default', title: 'index' })), + get: jest.fn(() => Promise.resolve(mockedIndices[0])), + find: jest.fn(() => Promise.resolve(mockedIndices || [])), + } as unknown) as IndexPatternsService; + + cachedIndexPatternFetcher = getCachedIndexPatternFetcher(indexPatternsService); + }); + + test('should return default index on no input value', async () => { + const value = await cachedIndexPatternFetcher(''); + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": Object { + "id": "default", + "title": "index", + }, + "indexPatternString": "index", + } + `); + }); + + describe('text-based index', () => { + test('should return the Kibana index if it exists', async () => { + mockedIndices = [ + { + id: 'indexId', + title: 'indexTitle', + }, + ] as IndexPattern[]; + + const value = await cachedIndexPatternFetcher('indexTitle'); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": Object { + "id": "indexId", + "title": "indexTitle", + }, + "indexPatternString": "indexTitle", + } + `); + }); + + test('should return only indexPatternString if Kibana index does not exist', async () => { + const value = await cachedIndexPatternFetcher('indexTitle'); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": undefined, + "indexPatternString": "indexTitle", + } + `); + }); + }); + + describe('object-based index', () => { + test('should return the Kibana index if it exists', async () => { + mockedIndices = [ + { + id: 'indexId', + title: 'indexTitle', + }, + ] as IndexPattern[]; + + const value = await cachedIndexPatternFetcher({ id: 'indexId' }); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": Object { + "id": "indexId", + "title": "indexTitle", + }, + "indexPatternString": "indexTitle", + } + `); + }); + + test('should return default index if Kibana index not found', async () => { + const value = await cachedIndexPatternFetcher({ id: 'indexId' }); + + expect(value).toMatchInlineSnapshot(` + Object { + "indexPattern": undefined, + "indexPatternString": "", + } + `); + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts new file mode 100644 index 0000000000000..68cbd93cdc614 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.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 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 { getIndexPatternKey, fetchIndexPattern } from '../../../../common/index_patterns_utils'; + +import type { IndexPatternsService } from '../../../../../data/server'; +import type { IndexPatternValue, FetchedIndexPattern } from '../../../../common/types'; + +export const getCachedIndexPatternFetcher = (indexPatternsService: IndexPatternsService) => { + const cache = new Map(); + + return async (indexPatternValue: IndexPatternValue): Promise => { + const key = getIndexPatternKey(indexPatternValue); + + if (cache.has(key)) { + return cache.get(key); + } + + const fetchedIndex = fetchIndexPattern(indexPatternValue, indexPatternsService); + + cache.set(indexPatternValue, fetchedIndex); + + return fetchedIndex; + }; +}; + +export type CachedIndexPatternFetcher = ReturnType; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts similarity index 57% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts index f95667612efa4..9003eb7fc2ced 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts @@ -6,21 +6,26 @@ * Side Public License, v 1. */ -import { - VisTypeTimeseriesRequestHandlerContext, - VisTypeTimeseriesVisDataRequest, -} from '../../../types'; -import { AbstractSearchStrategy, DefaultSearchCapabilities } from '../../search_strategies'; +import type { VisTypeTimeseriesVisDataRequest } from '../../../types'; +import type { AbstractSearchStrategy, DefaultSearchCapabilities } from '../index'; +import type { IndexPatternsService } from '../../../../../data/common'; +import type { CachedIndexPatternFetcher } from './cached_index_pattern_fetcher'; export interface FieldsFetcherServices { - requestContext: VisTypeTimeseriesRequestHandlerContext; + indexPatternsService: IndexPatternsService; + cachedIndexPatternFetcher: CachedIndexPatternFetcher; searchStrategy: AbstractSearchStrategy; capabilities: DefaultSearchCapabilities; } export const createFieldsFetcher = ( req: VisTypeTimeseriesVisDataRequest, - { capabilities, requestContext, searchStrategy }: FieldsFetcherServices + { + capabilities, + indexPatternsService, + searchStrategy, + cachedIndexPatternFetcher, + }: FieldsFetcherServices ) => { const fieldsCacheMap = new Map(); @@ -28,11 +33,11 @@ export const createFieldsFetcher = ( if (fieldsCacheMap.has(index)) { return fieldsCacheMap.get(index); } + const fetchedIndex = await cachedIndexPatternFetcher(index); const fields = await searchStrategy.getFieldsForWildcard( - requestContext, - req, - index, + fetchedIndex, + indexPatternsService, capabilities ); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/get_index_pattern.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/get_index_pattern.ts deleted file mode 100644 index 512494de290fd..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/get_index_pattern.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 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 { IndexPatternsService, IndexPattern } from '../../../../../data/server'; - -interface IndexPatternObjectDependencies { - indexPatternsService: IndexPatternsService; -} -export async function getIndexPatternObject( - indexPatternString: string, - { indexPatternsService }: IndexPatternObjectDependencies -) { - let indexPatternObject: IndexPattern | undefined | null; - - if (!indexPatternString) { - indexPatternObject = await indexPatternsService.getDefault(); - } else { - indexPatternObject = (await indexPatternsService.find(indexPatternString)).find( - (index) => index.title === indexPatternString - ); - } - - return { - indexPatternObject, - indexPatternString: indexPatternObject?.title || indexPatternString || '', - }; -} diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index f9a49bc322a29..a6e7c5b11ee64 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -10,29 +10,27 @@ import { get } from 'lodash'; import { SearchStrategyRegistry } from './search_strategy_registry'; import { AbstractSearchStrategy, DefaultSearchStrategy } from './strategies'; import { DefaultSearchCapabilities } from './capabilities/default_search_capabilities'; -import { Framework } from '../../plugin'; import { VisTypeTimeseriesRequest, VisTypeTimeseriesRequestHandlerContext } from '../../types'; const getPrivateField = (registry: SearchStrategyRegistry, field: string) => get(registry, field) as T; class MockSearchStrategy extends AbstractSearchStrategy { - checkForViability() { - return Promise.resolve({ + async checkForViability() { + return { isViable: true, capabilities: {}, - }); + }; } } describe('SearchStrategyRegister', () => { - const framework = {} as Framework; const requestContext = {} as VisTypeTimeseriesRequestHandlerContext; let registry: SearchStrategyRegistry; beforeAll(() => { registry = new SearchStrategyRegistry(); - registry.addStrategy(new DefaultSearchStrategy(framework)); + registry.addStrategy(new DefaultSearchStrategy()); }); test('should init strategies register', () => { @@ -47,12 +45,11 @@ describe('SearchStrategyRegister', () => { test('should return a DefaultSearchStrategy instance', async () => { const req = {} as VisTypeTimeseriesRequest; - const indexPattern = '*'; const { searchStrategy, capabilities } = (await registry.getViableStrategy( requestContext, req, - indexPattern + { indexPatternString: '*', indexPattern: undefined } ))!; expect(searchStrategy instanceof DefaultSearchStrategy).toBe(true); @@ -60,7 +57,7 @@ describe('SearchStrategyRegister', () => { }); test('should add a strategy if it is an instance of AbstractSearchStrategy', () => { - const anotherSearchStrategy = new MockSearchStrategy(framework); + const anotherSearchStrategy = new MockSearchStrategy(); const addedStrategies = registry.addStrategy(anotherSearchStrategy); expect(addedStrategies.length).toEqual(2); @@ -69,14 +66,13 @@ describe('SearchStrategyRegister', () => { test('should return a MockSearchStrategy instance', async () => { const req = {} as VisTypeTimeseriesRequest; - const indexPattern = '*'; - const anotherSearchStrategy = new MockSearchStrategy(framework); + const anotherSearchStrategy = new MockSearchStrategy(); registry.addStrategy(anotherSearchStrategy); const { searchStrategy, capabilities } = (await registry.getViableStrategy( requestContext, req, - indexPattern + { indexPatternString: '*', indexPattern: undefined } ))!; expect(searchStrategy instanceof MockSearchStrategy).toBe(true); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts index 11ff4b0a8a51f..4a013fd89735d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts @@ -6,14 +6,10 @@ * Side Public License, v 1. */ -import { extractIndexPatterns } from '../../../common/extract_index_patterns'; -import { PanelSchema } from '../../../common/types'; -import { - VisTypeTimeseriesRequest, - VisTypeTimeseriesRequestHandlerContext, - VisTypeTimeseriesVisDataRequest, -} from '../../types'; +import { VisTypeTimeseriesRequest, VisTypeTimeseriesRequestHandlerContext } from '../../types'; import { AbstractSearchStrategy } from './strategies'; +import { FetchedIndexPattern } from '../../../common/types'; + export class SearchStrategyRegistry { private strategies: AbstractSearchStrategy[] = []; @@ -27,13 +23,13 @@ export class SearchStrategyRegistry { async getViableStrategy( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesRequest, - indexPattern: string + fetchedIndexPattern: FetchedIndexPattern ) { for (const searchStrategy of this.strategies) { const { isViable, capabilities } = await searchStrategy.checkForViability( requestContext, req, - indexPattern + fetchedIndexPattern ); if (isViable) { @@ -44,14 +40,4 @@ export class SearchStrategyRegistry { } } } - - async getViableStrategyForPanel( - requestContext: VisTypeTimeseriesRequestHandlerContext, - req: VisTypeTimeseriesVisDataRequest, - panel: PanelSchema - ) { - const indexPattern = extractIndexPatterns(panel, panel.default_index_pattern).join(','); - - return this.getViableStrategy(requestContext, req, indexPattern); - } } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts index e7282eba58ec7..fb66e32447c22 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts @@ -6,48 +6,26 @@ * Side Public License, v 1. */ -const mockGetFieldsForWildcard = jest.fn(() => []); - -jest.mock('../../../../../data/server', () => ({ - indexPatterns: { - isNestedField: jest.fn(() => false), - }, - IndexPatternsFetcher: jest.fn().mockImplementation(() => ({ - getFieldsForWildcard: mockGetFieldsForWildcard, - })), -})); +import { IndexPatternsService } from '../../../../../data/common'; import { from } from 'rxjs'; -import { AbstractSearchStrategy, toSanitizedFieldType } from './abstract_search_strategy'; +import { AbstractSearchStrategy } from './abstract_search_strategy'; import type { IFieldType } from '../../../../../data/common'; -import type { FieldSpec, RuntimeField } from '../../../../../data/common'; -import { - VisTypeTimeseriesRequest, +import type { CachedIndexPatternFetcher } from '../lib/cached_index_pattern_fetcher'; +import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from '../../../types'; -import { Framework } from '../../../plugin'; -import { indexPatterns } from '../../../../../data/server'; class FooSearchStrategy extends AbstractSearchStrategy {} describe('AbstractSearchStrategy', () => { let abstractSearchStrategy: AbstractSearchStrategy; let mockedFields: IFieldType[]; - let indexPattern: string; let requestContext: VisTypeTimeseriesRequestHandlerContext; - let framework: Framework; beforeEach(() => { mockedFields = []; - framework = ({ - getIndexPatternsService: jest.fn(() => - Promise.resolve({ - find: jest.fn(() => []), - getDefault: jest.fn(() => {}), - }) - ), - } as unknown) as Framework; requestContext = ({ core: { elasticsearch: { @@ -60,7 +38,7 @@ describe('AbstractSearchStrategy', () => { search: jest.fn().mockReturnValue(from(Promise.resolve({}))), }, } as unknown) as VisTypeTimeseriesRequestHandlerContext; - abstractSearchStrategy = new FooSearchStrategy(framework); + abstractSearchStrategy = new FooSearchStrategy(); }); test('should init an AbstractSearchStrategy instance', () => { @@ -71,17 +49,15 @@ describe('AbstractSearchStrategy', () => { test('should return fields for wildcard', async () => { const fields = await abstractSearchStrategy.getFieldsForWildcard( - requestContext, - {} as VisTypeTimeseriesRequest, - indexPattern + { indexPatternString: '', indexPattern: undefined }, + ({ + getDefault: jest.fn(), + getFieldsForWildcard: jest.fn(() => Promise.resolve(mockedFields)), + } as unknown) as IndexPatternsService, + (() => Promise.resolve({}) as unknown) as CachedIndexPatternFetcher ); expect(fields).toEqual(mockedFields); - expect(mockGetFieldsForWildcard).toHaveBeenCalledWith({ - pattern: indexPattern, - metaFields: [], - fieldCapsOptions: { allow_no_indices: true }, - }); }); test('should return response', async () => { @@ -117,68 +93,4 @@ describe('AbstractSearchStrategy', () => { } ); }); - - describe('toSanitizedFieldType', () => { - const mockedField = { - lang: 'lang', - conflictDescriptions: {}, - aggregatable: true, - name: 'name', - type: 'type', - esTypes: ['long', 'geo'], - } as FieldSpec; - - test('should sanitize fields ', async () => { - const fields = [mockedField] as FieldSpec[]; - - expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(` - Array [ - Object { - "label": "name", - "name": "name", - "type": "type", - }, - ] - `); - }); - - test('should filter runtime fields', async () => { - const fields: FieldSpec[] = [ - { - ...mockedField, - runtimeField: {} as RuntimeField, - }, - ]; - - expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); - }); - - test('should filter non-aggregatable fields', async () => { - const fields: FieldSpec[] = [ - { - ...mockedField, - aggregatable: false, - }, - ]; - - expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); - }); - - test('should filter nested fields', async () => { - const fields: FieldSpec[] = [ - { - ...mockedField, - subType: { - nested: { - path: 'path', - }, - }, - }, - ]; - // @ts-expect-error - indexPatterns.isNestedField.mockReturnValue(true); - - expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); - }); - }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 5bc008091627f..26c3a6c7c8bf7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -6,37 +6,17 @@ * Side Public License, v 1. */ -import { indexPatterns, IndexPatternsFetcher } from '../../../../../data/server'; +import { IndexPatternsService } from '../../../../../data/server'; +import { toSanitizedFieldType } from '../../../../common/fields_utils'; -import type { Framework } from '../../../plugin'; -import type { FieldSpec } from '../../../../../data/common'; -import type { SanitizedFieldType } from '../../../../common/types'; +import type { FetchedIndexPattern } from '../../../../common/types'; import type { VisTypeTimeseriesRequest, VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from '../../../types'; -import { getIndexPatternObject } from '../lib/get_index_pattern'; - -export const toSanitizedFieldType = (fields: FieldSpec[]) => { - return fields - .filter( - (field) => - // Make sure to only include mapped fields, e.g. no index pattern runtime fields - !field.runtimeField && field.aggregatable && !indexPatterns.isNestedField(field) - ) - .map( - (field) => - ({ - name: field.name, - label: field.customLabel ?? field.name, - type: field.type, - } as SanitizedFieldType) - ); -}; export abstract class AbstractSearchStrategy { - constructor(private framework: Framework) {} async search( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesVisDataRequest, @@ -66,35 +46,25 @@ export abstract class AbstractSearchStrategy { checkForViability( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesRequest, - indexPattern: string + fetchedIndexPattern: FetchedIndexPattern ): Promise<{ isViable: boolean; capabilities: any }> { throw new TypeError('Must override method'); } async getFieldsForWildcard( - requestContext: VisTypeTimeseriesRequestHandlerContext, - req: VisTypeTimeseriesRequest, - indexPattern: string, + fetchedIndexPattern: FetchedIndexPattern, + indexPatternsService: IndexPatternsService, capabilities?: unknown, options?: Partial<{ type: string; rollupIndex: string; }> ) { - const indexPatternsFetcher = new IndexPatternsFetcher( - requestContext.core.elasticsearch.client.asCurrentUser - ); - const indexPatternsService = await this.framework.getIndexPatternsService(requestContext); - const { indexPatternObject } = await getIndexPatternObject(indexPattern, { - indexPatternsService, - }); - return toSanitizedFieldType( - indexPatternObject - ? indexPatternObject.getNonScriptedFields() - : await indexPatternsFetcher!.getFieldsForWildcard({ - pattern: indexPattern, - fieldCapsOptions: { allow_no_indices: true }, + fetchedIndexPattern.indexPattern + ? fetchedIndexPattern.indexPattern.getNonScriptedFields() + : await indexPatternsService.getFieldsForWildcard({ + pattern: fetchedIndexPattern.indexPatternString ?? '', metaFields: [], ...options, }) diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts index b9824355374e1..d7a4e6ddedc89 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { Framework } from '../../../plugin'; import { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, @@ -14,14 +13,13 @@ import { import { DefaultSearchStrategy } from './default_search_strategy'; describe('DefaultSearchStrategy', () => { - const framework = {} as Framework; const requestContext = {} as VisTypeTimeseriesRequestHandlerContext; let defaultSearchStrategy: DefaultSearchStrategy; let req: VisTypeTimeseriesVisDataRequest; beforeEach(() => { req = {} as VisTypeTimeseriesVisDataRequest; - defaultSearchStrategy = new DefaultSearchStrategy(framework); + defaultSearchStrategy = new DefaultSearchStrategy(); }); test('should init an DefaultSearchStrategy instance', () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index c925d8fcbb7c3..f95bf81b5c1d3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -8,25 +8,30 @@ import { AbstractSearchStrategy } from './abstract_search_strategy'; import { DefaultSearchCapabilities } from '../capabilities/default_search_capabilities'; -import { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequest } from '../../../types'; + +import type { IndexPatternsService } from '../../../../../data/server'; +import type { FetchedIndexPattern } from '../../../../common/types'; +import type { + VisTypeTimeseriesRequestHandlerContext, + VisTypeTimeseriesRequest, +} from '../../../types'; export class DefaultSearchStrategy extends AbstractSearchStrategy { - checkForViability( + async checkForViability( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesRequest ) { - return Promise.resolve({ + return { isViable: true, capabilities: new DefaultSearchCapabilities(req), - }); + }; } async getFieldsForWildcard( - requestContext: VisTypeTimeseriesRequestHandlerContext, - req: VisTypeTimeseriesRequest, - indexPattern: string, + fetchedIndexPattern: FetchedIndexPattern, + indexPatternsService: IndexPatternsService, capabilities?: unknown ) { - return super.getFieldsForWildcard(requestContext, req, indexPattern, capabilities); + return super.getFieldsForWildcard(fetchedIndexPattern, indexPatternsService, capabilities); } } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts index 403013cfb9e10..c798f58b0b67b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts @@ -7,8 +7,10 @@ */ import { RollupSearchStrategy } from './rollup_search_strategy'; -import { Framework } from '../../../plugin'; -import { + +import type { IndexPatternsService } from '../../../../../data/common'; +import type { CachedIndexPatternFetcher } from '../lib/cached_index_pattern_fetcher'; +import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from '../../../types'; @@ -49,12 +51,11 @@ describe('Rollup Search Strategy', () => { }, }, } as unknown) as VisTypeTimeseriesRequestHandlerContext; - const framework = {} as Framework; const indexPattern = 'indexPattern'; test('should create instance of RollupSearchRequest', () => { - const rollupSearchStrategy = new RollupSearchStrategy(framework); + const rollupSearchStrategy = new RollupSearchStrategy(); expect(rollupSearchStrategy).toBeDefined(); }); @@ -64,7 +65,7 @@ describe('Rollup Search Strategy', () => { const rollupIndex = 'rollupIndex'; beforeEach(() => { - rollupSearchStrategy = new RollupSearchStrategy(framework); + rollupSearchStrategy = new RollupSearchStrategy(); rollupSearchStrategy.getRollupData = jest.fn(() => Promise.resolve({ [rollupIndex]: { @@ -99,7 +100,7 @@ describe('Rollup Search Strategy', () => { const result = await rollupSearchStrategy.checkForViability( requestContext, {} as VisTypeTimeseriesVisDataRequest, - (null as unknown) as string + { indexPatternString: (null as unknown) as string, indexPattern: undefined } ); expect(result).toEqual({ @@ -113,7 +114,7 @@ describe('Rollup Search Strategy', () => { let rollupSearchStrategy: RollupSearchStrategy; beforeEach(() => { - rollupSearchStrategy = new RollupSearchStrategy(framework); + rollupSearchStrategy = new RollupSearchStrategy(); }); test('should return rollup data', async () => { @@ -140,7 +141,7 @@ describe('Rollup Search Strategy', () => { const rollupIndex = 'rollupIndex'; beforeEach(() => { - rollupSearchStrategy = new RollupSearchStrategy(framework); + rollupSearchStrategy = new RollupSearchStrategy(); fieldsCapabilities = { [rollupIndex]: { aggs: { @@ -154,9 +155,9 @@ describe('Rollup Search Strategy', () => { test('should return fields for wildcard', async () => { const fields = await rollupSearchStrategy.getFieldsForWildcard( - requestContext, - {} as VisTypeTimeseriesVisDataRequest, - indexPattern, + { indexPatternString: 'indexPattern', indexPattern: undefined }, + {} as IndexPatternsService, + (() => Promise.resolve({}) as unknown) as CachedIndexPatternFetcher, { fieldsCapabilities, rollupIndex, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts index 376d551624c8a..e6333ca420e0d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts @@ -6,19 +6,20 @@ * Side Public License, v 1. */ -import { getCapabilitiesForRollupIndices } from '../../../../../data/server'; -import { +import { getCapabilitiesForRollupIndices, IndexPatternsService } from '../../../../../data/server'; +import { AbstractSearchStrategy } from './abstract_search_strategy'; +import { RollupSearchCapabilities } from '../capabilities/rollup_search_capabilities'; + +import type { FetchedIndexPattern } from '../../../../common/types'; +import type { CachedIndexPatternFetcher } from '../lib/cached_index_pattern_fetcher'; +import type { VisTypeTimeseriesRequest, VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from '../../../types'; -import { AbstractSearchStrategy } from './abstract_search_strategy'; -import { RollupSearchCapabilities } from '../capabilities/rollup_search_capabilities'; const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); -const isIndexPatternValid = (indexPattern: string) => - indexPattern && typeof indexPattern === 'string' && !isIndexPatternContainsWildcard(indexPattern); export class RollupSearchStrategy extends AbstractSearchStrategy { async search( @@ -33,24 +34,33 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { requestContext: VisTypeTimeseriesRequestHandlerContext, indexPattern: string ) { - return requestContext.core.elasticsearch.client.asCurrentUser.rollup - .getRollupIndexCaps({ + try { + const { + body, + } = await requestContext.core.elasticsearch.client.asCurrentUser.rollup.getRollupIndexCaps({ index: indexPattern, - }) - .then((data) => data.body) - .catch(() => Promise.resolve({})); + }); + + return body; + } catch (e) { + return {}; + } } async checkForViability( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesRequest, - indexPattern: string + { indexPatternString, indexPattern }: FetchedIndexPattern ) { let isViable = false; let capabilities = null; - if (isIndexPatternValid(indexPattern)) { - const rollupData = await this.getRollupData(requestContext, indexPattern); + if ( + indexPatternString && + !isIndexPatternContainsWildcard(indexPatternString) && + (!indexPattern || indexPattern.type === 'rollup') + ) { + const rollupData = await this.getRollupData(requestContext, indexPatternString); const rollupIndices = getRollupIndices(rollupData); isViable = rollupIndices.length === 1; @@ -70,14 +80,14 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { } async getFieldsForWildcard( - requestContext: VisTypeTimeseriesRequestHandlerContext, - req: VisTypeTimeseriesRequest, - indexPattern: string, + fetchedIndexPattern: FetchedIndexPattern, + indexPatternsService: IndexPatternsService, + getCachedIndexPatternFetcher: CachedIndexPatternFetcher, capabilities?: unknown ) { - return super.getFieldsForWildcard(requestContext, req, indexPattern, capabilities, { + return super.getFieldsForWildcard(fetchedIndexPattern, indexPatternsService, capabilities, { type: 'rollup', - rollupIndex: indexPattern, + rollupIndex: fetchedIndexPattern.indexPatternString, }); } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts index c489a8d20b071..32086fbf4f5b4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts @@ -8,7 +8,6 @@ import { AnnotationItemsSchema, PanelSchema } from 'src/plugins/vis_type_timeseries/common/types'; import { buildAnnotationRequest } from './build_request_body'; -import { getIndexPatternObject } from '../../search_strategies/lib/get_index_pattern'; import { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequestServices, @@ -30,21 +29,20 @@ export async function getAnnotationRequestParams( esShardTimeout, esQueryConfig, capabilities, - indexPatternsService, uiSettings, + cachedIndexPatternFetcher, }: AnnotationServices ) { - const { - indexPatternObject, - indexPatternString, - } = await getIndexPatternObject(annotation.index_pattern!, { indexPatternsService }); + const { indexPattern, indexPatternString } = await cachedIndexPatternFetcher( + annotation.index_pattern + ); const request = await buildAnnotationRequest( req, panel, annotation, esQueryConfig, - indexPatternObject, + indexPattern, capabilities, uiSettings ); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js index 9b371a8901e81..ebab984ff25aa 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js @@ -10,8 +10,8 @@ import { AUTO_INTERVAL } from '../../../common/constants'; const DEFAULT_TIME_FIELD = '@timestamp'; -export function getIntervalAndTimefield(panel, series = {}, indexPatternObject) { - const getDefaultTimeField = () => indexPatternObject?.timeFieldName ?? DEFAULT_TIME_FIELD; +export function getIntervalAndTimefield(panel, series = {}, indexPattern) { + const getDefaultTimeField = () => indexPattern?.timeFieldName ?? DEFAULT_TIME_FIELD; const timeField = (series.override_index_pattern && series.series_time_field) || diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts index f521de632b1f8..13dc1207f51de 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts @@ -21,6 +21,7 @@ import type { VisTypeTimeseriesRequestServices, } from '../../types'; import type { PanelSchema } from '../../../common/types'; +import { PANEL_TYPES } from '../../../common/panel_types'; export async function getSeriesData( requestContext: VisTypeTimeseriesRequestHandlerContext, @@ -28,10 +29,12 @@ export async function getSeriesData( panel: PanelSchema, services: VisTypeTimeseriesRequestServices ) { - const strategy = await services.searchStrategyRegistry.getViableStrategyForPanel( + const panelIndex = await services.cachedIndexPatternFetcher(panel.index_pattern); + + const strategy = await services.searchStrategyRegistry.getViableStrategy( requestContext, req, - panel + panelIndex ); if (!strategy) { @@ -50,14 +53,15 @@ export async function getSeriesData( try { const bodiesPromises = getActiveSeries(panel).map((series) => - getSeriesRequestParams(req, panel, series, capabilities, services) + getSeriesRequestParams(req, panel, panelIndex, series, capabilities, services) ); const searches = await Promise.all(bodiesPromises); const data = await searchStrategy.search(requestContext, req, searches); const handleResponseBodyFn = handleResponseBody(panel, req, { - requestContext, + indexPatternsService: services.indexPatternsService, + cachedIndexPatternFetcher: services.cachedIndexPatternFetcher, searchStrategy, capabilities, }); @@ -70,7 +74,7 @@ export async function getSeriesData( let annotations = null; - if (panel.annotations && panel.annotations.length) { + if (panel.type === PANEL_TYPES.TIMESERIES && panel.annotations && panel.annotations.length) { annotations = await getAnnotations({ req, panel, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts index a35a3246b0dd3..0cc1188086b7b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts @@ -16,8 +16,8 @@ import { buildRequestBody } from './table/build_request_body'; import { handleErrorResponse } from './handle_error_response'; // @ts-expect-error import { processBucket } from './table/process_bucket'; -import { getIndexPatternObject } from '../search_strategies/lib/get_index_pattern'; -import { createFieldsFetcher } from './helpers/fields_fetcher'; + +import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher'; import { extractFieldLabel } from '../../../common/calculate_label'; import type { VisTypeTimeseriesRequestHandlerContext, @@ -32,12 +32,12 @@ export async function getTableData( panel: PanelSchema, services: VisTypeTimeseriesRequestServices ) { - const panelIndexPattern = panel.index_pattern; + const panelIndex = await services.cachedIndexPatternFetcher(panel.index_pattern); const strategy = await services.searchStrategyRegistry.getViableStrategy( requestContext, req, - panelIndexPattern + panelIndex ); if (!strategy) { @@ -49,15 +49,17 @@ export async function getTableData( } const { searchStrategy, capabilities } = strategy; - const { indexPatternObject } = await getIndexPatternObject(panelIndexPattern, { + + const extractFields = createFieldsFetcher(req, { indexPatternsService: services.indexPatternsService, + cachedIndexPatternFetcher: services.cachedIndexPatternFetcher, + searchStrategy, + capabilities, }); - const extractFields = createFieldsFetcher(req, { requestContext, searchStrategy, capabilities }); - const calculatePivotLabel = async () => { - if (panel.pivot_id && indexPatternObject?.title) { - const fields = await extractFields(indexPatternObject.title); + if (panel.pivot_id && panelIndex.indexPattern?.title) { + const fields = await extractFields(panelIndex.indexPattern.title); return extractFieldLabel(fields, panel.pivot_id); } @@ -75,7 +77,7 @@ export async function getTableData( req, panel, services.esQueryConfig, - indexPatternObject, + panelIndex.indexPattern, capabilities, services.uiSettings ); @@ -83,7 +85,7 @@ export async function getTableData( const [resp] = await searchStrategy.search(requestContext, req, [ { body, - index: panelIndexPattern, + index: panelIndex.indexPatternString, }, ]); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js index 0d100f6310b99..48b33c1e787e9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -18,7 +18,7 @@ export function dateHistogram( panel, annotation, esQueryConfig, - indexPatternObject, + indexPattern, capabilities, uiSettings ) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index 9ff0325b60e82..dab9a24d06c0f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -19,7 +19,7 @@ export function dateHistogram( panel, series, esQueryConfig, - indexPatternObject, + indexPattern, capabilities, uiSettings ) { @@ -27,11 +27,7 @@ export function dateHistogram( const maxBarsUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { timeField, interval, maxBars } = getIntervalAndTimefield( - panel, - series, - indexPatternObject - ); + const { timeField, interval, maxBars } = getIntervalAndTimefield(panel, series, indexPattern); const { bucketSize, intervalString } = getBucketSize( req, interval, @@ -68,7 +64,7 @@ export function dateHistogram( overwrite(doc, `aggs.${series.id}.meta`, { timeField, intervalString, - index: indexPatternObject?.title, + index: indexPattern?.title, bucketSize, seriesId: series.id, }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index d653f6acf6f3e..945c57b2341f3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -16,7 +16,7 @@ describe('dateHistogram(req, panel, series)', () => { let req; let capabilities; let config; - let indexPatternObject; + let indexPattern; let uiSettings; beforeEach(() => { @@ -39,7 +39,7 @@ describe('dateHistogram(req, panel, series)', () => { allowLeadingWildcards: true, queryStringOptions: {}, }; - indexPatternObject = {}; + indexPattern = {}; capabilities = new DefaultSearchCapabilities(req); uiSettings = { get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50), @@ -49,15 +49,9 @@ describe('dateHistogram(req, panel, series)', () => { test('calls next when finished', async () => { const next = jest.fn(); - await dateHistogram( - req, - panel, - series, - config, - indexPatternObject, - capabilities, - uiSettings - )(next)({}); + await dateHistogram(req, panel, series, config, indexPattern, capabilities, uiSettings)(next)( + {} + ); expect(next.mock.calls.length).toEqual(1); }); @@ -69,7 +63,7 @@ describe('dateHistogram(req, panel, series)', () => { panel, series, config, - indexPatternObject, + indexPattern, capabilities, uiSettings )(next)({}); @@ -110,7 +104,7 @@ describe('dateHistogram(req, panel, series)', () => { panel, series, config, - indexPatternObject, + indexPattern, capabilities, uiSettings )(next)({}); @@ -154,7 +148,7 @@ describe('dateHistogram(req, panel, series)', () => { panel, series, config, - indexPatternObject, + indexPattern, capabilities, uiSettings )(next)({}); @@ -198,7 +192,7 @@ describe('dateHistogram(req, panel, series)', () => { panel, series, config, - indexPatternObject, + indexPattern, capabilities, uiSettings )(next)({}); @@ -216,7 +210,7 @@ describe('dateHistogram(req, panel, series)', () => { panel, series, config, - indexPatternObject, + indexPattern, capabilities, uiSettings )(next)({}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js index 31ae988718a27..4639af9db83b8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js @@ -12,19 +12,19 @@ import { esQuery } from '../../../../../../data/server'; const filter = (metric) => metric.type === 'filter_ratio'; -export function ratios(req, panel, series, esQueryConfig, indexPatternObject) { +export function ratios(req, panel, series, esQueryConfig, indexPattern) { return (next) => (doc) => { if (series.metrics.some(filter)) { series.metrics.filter(filter).forEach((metric) => { overwrite( doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`, - esQuery.buildEsQuery(indexPatternObject, metric.numerator, [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, metric.numerator, [], esQueryConfig) ); overwrite( doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`, - esQuery.buildEsQuery(indexPatternObject, metric.denominator, [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, metric.denominator, [], esQueryConfig) ); let numeratorPath = `${metric.id}-numerator>_count`; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js index 9e0dd4f76c13f..345488ec01d5e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js @@ -13,7 +13,7 @@ describe('ratios(req, panel, series, esQueryConfig, indexPatternObject)', () => let series; let req; let esQueryConfig; - let indexPatternObject; + let indexPattern; beforeEach(() => { panel = { time_field: 'timestamp', @@ -47,18 +47,18 @@ describe('ratios(req, panel, series, esQueryConfig, indexPatternObject)', () => queryStringOptions: { analyze_wildcard: true }, ignoreFilterIfFieldNotInIndex: false, }; - indexPatternObject = {}; + indexPattern = {}; }); test('calls next when finished', () => { const next = jest.fn(); - ratios(req, panel, series, esQueryConfig, indexPatternObject)(next)({}); + ratios(req, panel, series, esQueryConfig, indexPattern)(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns filter ratio aggs', () => { const next = (doc) => doc; - const doc = ratios(req, panel, series, esQueryConfig, indexPatternObject)(next)({}); + const doc = ratios(req, panel, series, esQueryConfig, indexPattern)(next)({}); expect(doc).toEqual({ aggs: { test: { @@ -135,7 +135,7 @@ describe('ratios(req, panel, series, esQueryConfig, indexPatternObject)', () => test('returns empty object when field is not set', () => { delete series.metrics[0].field; const next = (doc) => doc; - const doc = ratios(req, panel, series, esQueryConfig, indexPatternObject)(next)({}); + const doc = ratios(req, panel, series, esQueryConfig, indexPattern)(next)({}); expect(doc).toEqual({ aggs: { test: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js index 649b3cee6ea3e..86b691f6496c9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js @@ -17,14 +17,14 @@ export function metricBuckets( panel, series, esQueryConfig, - indexPatternObject, + indexPattern, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { interval } = getIntervalAndTimefield(panel, series, indexPattern); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); series.metrics diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js index 1d67df7c92eb6..ce61374c0b124 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -56,14 +56,14 @@ export function positiveRate( panel, series, esQueryConfig, - indexPatternObject, + indexPattern, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { interval } = getIntervalAndTimefield(panel, series, indexPattern); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); if (series.metrics.some(filter)) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js index cb12aa3513b91..d0e92c9157cb5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js @@ -10,16 +10,16 @@ import { offsetTime } from '../../offset_time'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { esQuery } from '../../../../../../data/server'; -export function query(req, panel, series, esQueryConfig, indexPatternObject) { +export function query(req, panel, series, esQueryConfig, indexPattern) { return (next) => (doc) => { - const { timeField } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { timeField } = getIntervalAndTimefield(panel, series, indexPattern); const { from, to } = offsetTime(req, series.offset_time); doc.size = 0; const ignoreGlobalFilter = panel.ignore_global_filter || series.ignore_global_filter; const queries = !ignoreGlobalFilter ? req.body.query : []; const filters = !ignoreGlobalFilter ? req.body.filters : []; - doc.query = esQuery.buildEsQuery(indexPatternObject, queries, filters, esQueryConfig); + doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig); const timerange = { range: { @@ -34,13 +34,13 @@ export function query(req, panel, series, esQueryConfig, indexPatternObject) { if (panel.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPatternObject, [panel.filter], [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig) ); } if (series.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPatternObject, [series.filter], [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, [series.filter], [], esQueryConfig) ); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js index 315ccdfc13a47..401344d48f865 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js @@ -17,13 +17,13 @@ export function siblingBuckets( panel, series, esQueryConfig, - indexPatternObject, + indexPattern, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { interval } = getIntervalAndTimefield(panel, series, indexPattern); const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings); series.metrics diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index 0ae6d113e28e4..5518065643172 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -15,20 +15,13 @@ import { calculateAggRoot } from './calculate_agg_root'; import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; const { dateHistogramInterval } = search.aggs; -export function dateHistogram( - req, - panel, - esQueryConfig, - indexPatternObject, - capabilities, - uiSettings -) { +export function dateHistogram(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPattern); const meta = { timeField, - index: indexPatternObject?.title, + index: indexPattern?.title, }; const getDateHistogramForLastBucketMode = () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js index 7b3ac16cd6561..abb5971908771 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -13,7 +13,7 @@ import { calculateAggRoot } from './calculate_agg_root'; const filter = (metric) => metric.type === 'filter_ratio'; -export function ratios(req, panel, esQueryConfig, indexPatternObject) { +export function ratios(req, panel, esQueryConfig, indexPattern) { return (next) => (doc) => { panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); @@ -22,12 +22,12 @@ export function ratios(req, panel, esQueryConfig, indexPatternObject) { overwrite( doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, - esQuery.buildEsQuery(indexPatternObject, metric.numerator, [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, metric.numerator, [], esQueryConfig) ); overwrite( doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, - esQuery.buildEsQuery(indexPatternObject, metric.denominator, [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, metric.denominator, [], esQueryConfig) ); let numeratorPath = `${metric.id}-numerator>_count`; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js index 53149a31603ef..5ce508bd9b279 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js @@ -13,17 +13,10 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; import { UI_SETTINGS } from '../../../../../../data/common'; -export function metricBuckets( - req, - panel, - esQueryConfig, - indexPatternObject, - capabilities, - uiSettings -) { +export function metricBuckets(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { interval } = getIntervalAndTimefield(panel, {}, indexPattern); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js index 8c7a0f5e2367f..176721e7b563a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js @@ -12,17 +12,10 @@ import { calculateAggRoot } from './calculate_agg_root'; import { createPositiveRate, filter } from '../series/positive_rate'; import { UI_SETTINGS } from '../../../../../../data/common'; -export function positiveRate( - req, - panel, - esQueryConfig, - indexPatternObject, - capabilities, - uiSettings -) { +export function positiveRate(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { interval } = getIntervalAndTimefield(panel, {}, indexPattern); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js index a0118c5037d34..76df07b76e80e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js @@ -10,16 +10,16 @@ import { getTimerange } from '../../helpers/get_timerange'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { esQuery } from '../../../../../../data/server'; -export function query(req, panel, esQueryConfig, indexPatternObject) { +export function query(req, panel, esQueryConfig, indexPattern) { return (next) => (doc) => { - const { timeField } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { timeField } = getIntervalAndTimefield(panel, {}, indexPattern); const { from, to } = getTimerange(req); doc.size = 0; const queries = !panel.ignore_global_filter ? req.body.query : []; const filters = !panel.ignore_global_filter ? req.body.filters : []; - doc.query = esQuery.buildEsQuery(indexPatternObject, queries, filters, esQueryConfig); + doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig); const timerange = { range: { @@ -33,7 +33,7 @@ export function query(req, panel, esQueryConfig, indexPatternObject) { doc.query.bool.must.push(timerange); if (panel.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPatternObject, [panel.filter], [], esQueryConfig) + esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig) ); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js index d205f0679a908..5539f16df41e0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js @@ -13,17 +13,10 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; import { UI_SETTINGS } from '../../../../../../data/common'; -export function siblingBuckets( - req, - panel, - esQueryConfig, - indexPatternObject, - capabilities, - uiSettings -) { +export function siblingBuckets(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { interval } = getIntervalAndTimefield(panel, {}, indexPattern); const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings); panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index 968fe01565b04..d97af8ac748f4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -79,13 +79,13 @@ describe('buildRequestBody(req)', () => { allowLeadingWildcards: true, queryStringOptions: {}, }; - const indexPatternObject = {}; + const indexPattern = {}; const doc = await buildRequestBody( { body }, panel, series, config, - indexPatternObject, + indexPattern, capabilities, { get: async () => 50, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts index ae846b5b4b817..1f2735da8fb06 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts @@ -6,43 +6,46 @@ * Side Public License, v 1. */ -import { PanelSchema, SeriesItemsSchema } from '../../../../common/types'; import { buildRequestBody } from './build_request_body'; -import { getIndexPatternObject } from '../../../lib/search_strategies/lib/get_index_pattern'; -import { VisTypeTimeseriesRequestServices, VisTypeTimeseriesVisDataRequest } from '../../../types'; -import { DefaultSearchCapabilities } from '../../search_strategies'; + +import type { FetchedIndexPattern, PanelSchema, SeriesItemsSchema } from '../../../../common/types'; +import type { + VisTypeTimeseriesRequestServices, + VisTypeTimeseriesVisDataRequest, +} from '../../../types'; +import type { DefaultSearchCapabilities } from '../../search_strategies'; export async function getSeriesRequestParams( req: VisTypeTimeseriesVisDataRequest, panel: PanelSchema, + panelIndex: FetchedIndexPattern, series: SeriesItemsSchema, capabilities: DefaultSearchCapabilities, { esQueryConfig, esShardTimeout, uiSettings, - indexPatternsService, + cachedIndexPatternFetcher, }: VisTypeTimeseriesRequestServices ) { - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + let seriesIndex = panelIndex; - const { indexPatternObject, indexPatternString } = await getIndexPatternObject(indexPattern, { - indexPatternsService, - }); + if (series.override_index_pattern) { + seriesIndex = await cachedIndexPatternFetcher(series.series_index_pattern ?? ''); + } const request = await buildRequestBody( req, panel, series, esQueryConfig, - indexPatternObject, + seriesIndex.indexPattern, capabilities, uiSettings ); return { - index: indexPatternString, + index: seriesIndex.indexPatternString, body: { ...request, timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts index 22e0372c23526..49f1ec0f93de5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts @@ -12,7 +12,10 @@ import { PanelSchema } from '../../../../common/types'; import { buildProcessorFunction } from '../build_processor_function'; // @ts-expect-error import { processors } from '../response_processors/series'; -import { createFieldsFetcher, FieldsFetcherServices } from './../helpers/fields_fetcher'; +import { + createFieldsFetcher, + FieldsFetcherServices, +} from '../../search_strategies/lib/fields_fetcher'; import { VisTypeTimeseriesVisDataRequest } from '../../../types'; export function handleResponseBody( diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 71b76dddbca6a..8bc752e944709 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -37,6 +37,8 @@ import { } from './lib/search_strategies'; import { TimeseriesVisData, VisPayload } from '../common/types'; +import { registerTimeseriesUsageCollector } from './usage_collector'; + export interface LegacySetup { server: Server; } @@ -111,12 +113,16 @@ export class VisTypeTimeseriesPlugin implements Plugin { }, }; - searchStrategyRegistry.addStrategy(new DefaultSearchStrategy(framework)); - searchStrategyRegistry.addStrategy(new RollupSearchStrategy(framework)); + searchStrategyRegistry.addStrategy(new DefaultSearchStrategy()); + searchStrategyRegistry.addStrategy(new RollupSearchStrategy()); visDataRoutes(router, framework); fieldsRoutes(router, framework); + if (plugins.usageCollection) { + registerTimeseriesUsageCollector(plugins.usageCollection, globalConfig$); + } + return { getVisData: async ( requestContext: VisTypeTimeseriesRequestHandlerContext, diff --git a/src/plugins/vis_type_timeseries/server/types.ts b/src/plugins/vis_type_timeseries/server/types.ts index da32669b3855d..7b42cf61d52b3 100644 --- a/src/plugins/vis_type_timeseries/server/types.ts +++ b/src/plugins/vis_type_timeseries/server/types.ts @@ -6,14 +6,19 @@ * Side Public License, v 1. */ +import { Observable } from 'rxjs'; +import { SharedGlobalConfig } from 'kibana/server'; import type { IRouter, IUiSettingsClient, KibanaRequest } from 'src/core/server'; import type { DataRequestHandlerContext, EsQueryConfig, IndexPatternsService, } from '../../data/server'; -import { VisPayload } from '../common/types'; -import { SearchStrategyRegistry } from './lib/search_strategies'; +import type { VisPayload } from '../common/types'; +import type { SearchStrategyRegistry } from './lib/search_strategies'; +import type { CachedIndexPatternFetcher } from './lib/search_strategies/lib/cached_index_pattern_fetcher'; + +export type ConfigObservable = Observable; export type VisTypeTimeseriesRequestHandlerContext = DataRequestHandlerContext; export type VisTypeTimeseriesRouter = IRouter; @@ -29,4 +34,5 @@ export interface VisTypeTimeseriesRequestServices { uiSettings: IUiSettingsClient; indexPatternsService: IndexPatternsService; searchStrategyRegistry: SearchStrategyRegistry; + cachedIndexPatternFetcher: CachedIndexPatternFetcher; } diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.mock.ts b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.mock.ts new file mode 100644 index 0000000000000..bb52d215c67e8 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 mockStats = { somestat: 1 }; +export const mockGetStats = jest.fn().mockResolvedValue(mockStats); + +jest.doMock('./get_usage_collector', () => ({ + getStats: mockGetStats, +})); diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts new file mode 100644 index 0000000000000..8ecc02072905f --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { getStats } from './get_usage_collector'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { TIME_RANGE_DATA_MODES } from '../../common/timerange_data_modes'; + +const mockedSavedObjects = [ + { + _id: 'visualization:timeseries-123', + _source: { + type: 'visualization', + visualization: { + visState: JSON.stringify({ + type: 'metrics', + title: 'TSVB visualization 1', + params: { + time_range_mode: TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + }, + }), + }, + }, + }, + { + _id: 'visualization:timeseries-321', + _source: { + type: 'visualization', + visualization: { + visState: JSON.stringify({ + type: 'metrics', + title: 'TSVB visualization 2', + params: { + time_range_mode: TIME_RANGE_DATA_MODES.LAST_VALUE, + }, + }), + }, + }, + }, + { + _id: 'visualization:timeseries-456', + _source: { + type: 'visualization', + visualization: { + visState: JSON.stringify({ + type: 'metrics', + title: 'TSVB visualization 3', + params: { + time_range_mode: undefined, + }, + }), + }, + }, + }, +]; + +const mockedSavedObjectsByValue = [ + { + attributes: { + panelsJSON: JSON.stringify({ + type: 'visualization', + embeddableConfig: { + savedVis: { + type: 'metrics', + params: { + time_range_mode: TIME_RANGE_DATA_MODES.LAST_VALUE, + }, + }, + }, + }), + }, + }, + { + attributes: { + panelsJSON: JSON.stringify({ + type: 'visualization', + embeddableConfig: { + savedVis: { + type: 'metrics', + params: { + time_range_mode: TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + }, + }, + }, + }), + }, + }, +]; + +const getMockCollectorFetchContext = (hits?: unknown[], savedObjectsByValue: unknown[] = []) => { + const fetchParamsMock = createCollectorFetchContextMock(); + + fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: { hits: { hits } } }); + fetchParamsMock.soClient.find = jest.fn().mockResolvedValue({ + saved_objects: savedObjectsByValue, + }); + return fetchParamsMock; +}; + +describe('Timeseries visualization usage collector', () => { + const mockIndex = 'mock_index'; + + test('Returns undefined when no results found (undefined)', async () => { + const mockCollectorFetchContext = getMockCollectorFetchContext([], []); + const result = await getStats( + mockCollectorFetchContext.esClient, + mockCollectorFetchContext.soClient, + mockIndex + ); + + expect(result).toBeUndefined(); + }); + + test('Returns undefined when no timeseries saved objects found', async () => { + const mockCollectorFetchContext = getMockCollectorFetchContext( + [ + { + _id: 'visualization:myvis-123', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "area"}' }, + }, + }, + ], + [ + { + attributes: { + panelsJSON: JSON.stringify({ + type: 'visualization', + embeddableConfig: { + savedVis: { + type: 'area', + }, + }, + }), + }, + }, + ] + ); + const result = await getStats( + mockCollectorFetchContext.esClient, + mockCollectorFetchContext.soClient, + mockIndex + ); + + expect(result).toBeUndefined(); + }); + + test('Summarizes visualizations response data', async () => { + const mockCollectorFetchContext = getMockCollectorFetchContext( + mockedSavedObjects, + mockedSavedObjectsByValue + ); + const result = await getStats( + mockCollectorFetchContext.esClient, + mockCollectorFetchContext.soClient, + mockIndex + ); + + expect(result).toMatchObject({ + timeseries_use_last_value_mode_total: 3, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts new file mode 100644 index 0000000000000..c1a8715f72227 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts @@ -0,0 +1,86 @@ +/* + * Copyright 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 { ElasticsearchClient } from 'src/core/server'; +import { SavedObjectsClientContract, ISavedObjectsRepository } from 'kibana/server'; +import { TIME_RANGE_DATA_MODES } from '../../common/timerange_data_modes'; +import { findByValueEmbeddables } from '../../../dashboard/server'; + +export interface TimeseriesUsage { + timeseries_use_last_value_mode_total: number; +} + +interface VisState { + type?: string; + params?: any; +} + +export const getStats = async ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract | ISavedObjectsRepository, + index: string +): Promise => { + const timeseriesUsage = { + timeseries_use_last_value_mode_total: 0, + }; + + const searchParams = { + size: 10000, + index, + ignoreUnavailable: true, + filterPath: ['hits.hits._id', 'hits.hits._source.visualization'], + body: { + query: { + bool: { + filter: { term: { type: 'visualization' } }, + }, + }, + }, + }; + + const { body: esResponse } = await esClient.search<{ + visualization: { visState: string }; + updated_at: string; + }>(searchParams); + + function telemetryUseLastValueMode(visState: VisState) { + if ( + visState.type === 'metrics' && + visState.params.type !== 'timeseries' && + (!visState.params.time_range_mode || + visState.params.time_range_mode === TIME_RANGE_DATA_MODES.LAST_VALUE) + ) { + timeseriesUsage.timeseries_use_last_value_mode_total++; + } + } + + if (esResponse?.hits?.hits?.length) { + for (const hit of esResponse.hits.hits) { + if (hit._source && 'visualization' in hit._source) { + const { visualization } = hit._source!; + + let visState: VisState = {}; + try { + visState = JSON.parse(visualization?.visState ?? '{}'); + } catch (e) { + // invalid visState + } + + telemetryUseLastValueMode(visState); + } + } + } + + const byValueVisualizations = await findByValueEmbeddables(soClient, 'visualization'); + + for (const item of byValueVisualizations) { + telemetryUseLastValueMode(item.savedVis as VisState); + } + + return timeseriesUsage.timeseries_use_last_value_mode_total ? timeseriesUsage : undefined; +}; diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/index.ts b/src/plugins/vis_type_timeseries/server/usage_collector/index.ts new file mode 100644 index 0000000000000..7f72662e154ea --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/usage_collector/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { registerTimeseriesUsageCollector } from './register_timeseries_collector'; diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts new file mode 100644 index 0000000000000..2612a3882af2d --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.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 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 { of } from 'rxjs'; +import { mockStats, mockGetStats } from './get_usage_collector.mock'; +import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { registerTimeseriesUsageCollector } from './register_timeseries_collector'; +import { ConfigObservable } from '../types'; + +describe('registerTimeseriesUsageCollector', () => { + const mockIndex = 'mock_index'; + const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; + + it('makes a usage collector and registers it`', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); + expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); + }); + + it('makeUsageCollector configs fit the shape', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ + type: 'vis_type_timeseries', + isReady: expect.any(Function), + fetch: expect.any(Function), + schema: expect.any(Object), + }); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('makeUsageCollector config.isReady returns true', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('makeUsageCollector config.fetch calls getStats', async () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; + const mockedCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext); + expect(mockGetStats).toBeCalledTimes(1); + expect(mockGetStats).toBeCalledWith( + mockedCollectorFetchContext.esClient, + mockedCollectorFetchContext.soClient, + mockIndex + ); + expect(fetchResult).toBe(mockStats); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts new file mode 100644 index 0000000000000..5edeb6654020e --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { first } from 'rxjs/operators'; +import { getStats, TimeseriesUsage } from './get_usage_collector'; +import { ConfigObservable } from '../types'; + +export function registerTimeseriesUsageCollector( + collectorSet: UsageCollectionSetup, + config: ConfigObservable +) { + const collector = collectorSet.makeUsageCollector({ + type: 'vis_type_timeseries', + isReady: () => true, + schema: { + timeseries_use_last_value_mode_total: { + type: 'long', + _meta: { description: 'Number of TSVB visualizations using "last value" as a time range' }, + }, + }, + fetch: async ({ esClient, soClient }) => { + const { index } = (await config.pipe(first()).toPromise()).kibana; + + return await getStats(esClient, soClient, index); + }, + }); + + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx index efb41c470024b..f5b0f614458fd 100644 --- a/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx @@ -11,6 +11,8 @@ import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } fr import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { getDocLinks } from '../services'; + function VegaHelpMenu() { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); @@ -30,7 +32,7 @@ function VegaHelpMenu() { const items = [ diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 042ffac583e98..8590b51d3b5ff 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SearchResponse, SearchParams } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; @@ -17,7 +17,7 @@ import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; interface Body { - aggs?: SearchParams['body']['aggs']; + aggs?: Record; query?: Query; timeout?: string; } @@ -76,7 +76,7 @@ interface Projection { interface RequestDataObject { name?: string; url?: TUrlData; - values: SearchResponse; + values: estypes.SearchResponse; } type ContextVarsObjectProps = diff --git a/src/plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson index ace1950f4e909..834bfdc4ff278 100644 --- a/src/plugins/vis_type_vega/public/default.spec.hjson +++ b/src/plugins/vis_type_vega/public/default.spec.hjson @@ -6,7 +6,7 @@ Welcome to Vega visualizations. Here you can design your own dataviz from scrat This example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner. */ - $schema: https://vega.github.io/schema/vega-lite/v4.json + $schema: https://vega.github.io/schema/vega-lite/v5.json title: Event counts from all indexes // Define the data source diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 0204c2c90b71b..f935362d21604 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -19,6 +19,7 @@ import { setUISettings, setInjectedMetadata, setMapServiceSettings, + setDocLinks, } from './services'; import { createVegaFn } from './vega_fn'; @@ -96,5 +97,6 @@ export class VegaPlugin implements Plugin { setNotifications(core.notifications); setData(data); setInjectedMetadata(core.injectedMetadata); + setDocLinks(core.docLinks); } } diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index c47378282932b..f67fe4794e783 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/public'; +import { CoreStart, NotificationsStart, IUiSettingsClient, DocLinksStart } from 'src/core/public'; import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../kibana_utils/public'; @@ -35,3 +35,5 @@ export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ }>('InjectedVars'); export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; + +export const [getDocLinks, setDocLinks] = createGetterSetter('docLinks'); diff --git a/src/plugins/vis_type_vega/public/test_utils/default.spec.json b/src/plugins/vis_type_vega/public/test_utils/default.spec.json index 8cf763647115f..c1db62fa8035a 100644 --- a/src/plugins/vis_type_vega/public/test_utils/default.spec.json +++ b/src/plugins/vis_type_vega/public/test_utils/default.spec.json @@ -1,5 +1,5 @@ { - "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "title": "Event counts from all indexes", "data": { "url": { @@ -30,7 +30,7 @@ "x": { "field": "key", "type": "temporal", - "axis": { "title": false } + "axis": { "title": null } }, "y": { "field": "doc_count", diff --git a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json index 5394f009b074f..5a5e72f59022b 100644 --- a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json +++ b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json @@ -1,5 +1,5 @@ { - "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "data": { "format": {"property": "aggregations.time_buckets.buckets"}, "values": { diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index a83409f936078..ce815cba4a4e2 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -20,7 +20,7 @@ const mockedSavedObjects = [ visState: JSON.stringify({ type: 'vega', params: { - spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v4.json" }', + spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v5.json" }', }, }), }, diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts index 8d3512aa2138e..d5f8d978d5252 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts @@ -7,14 +7,12 @@ */ import { parse } from 'hjson'; -import { SearchResponse } from 'elasticsearch'; import { ElasticsearchClient, SavedObject } from 'src/core/server'; import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; type UsageCollectorDependencies = Pick; -type ESResponse = SearchResponse<{ visualization: { visState: string } }>; type VegaType = 'vega' | 'vega-lite'; function isVegaType(attributes: any): attributes is VegaSavedObjectAttributes { @@ -80,7 +78,9 @@ export const getStats = async ( }, }; - const { body: esResponse } = await esClient.search(searchParams); + const { body: esResponse } = await esClient.search<{ visualization: { visState: string } }>( + searchParams + ); const size = esResponse?.hits?.hits?.length ?? 0; if (!size) { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 349e024f31c31..c2b9fcd77757a 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -12,6 +12,8 @@ import { first } from 'rxjs/operators'; import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { SavedObjectAttributes } from '../../../../core/public'; import { extractSearchSourceReferences } from '../../../data/public'; +import { SavedObjectReference } from '../../../../core/public'; + import { EmbeddableFactoryDefinition, EmbeddableOutput, @@ -38,6 +40,12 @@ import { } from '../services'; import { showNewVisModal } from '../wizard'; import { convertToSerializedVis } from '../saved_visualizations/_saved_vis'; +import { + extractControlsReferences, + extractTimeSeriesReferences, + injectTimeSeriesReferences, + injectControlsReferences, +} from '../saved_visualizations/saved_visualization_references'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; @@ -239,6 +247,19 @@ export class VisualizeEmbeddableFactory ); } + public inject(_state: EmbeddableStateWithType, references: SavedObjectReference[]) { + const state = (_state as unknown) as VisualizeInput; + + const { type, params } = state.savedVis ?? {}; + + if (type && params) { + injectControlsReferences(type, params, references); + injectTimeSeriesReferences(type, params, references); + } + + return _state; + } + public extract(_state: EmbeddableStateWithType) { const state = (_state as unknown) as VisualizeInput; const references = []; @@ -259,19 +280,11 @@ export class VisualizeEmbeddableFactory }); } - if (state.savedVis?.params.controls) { - const controls = state.savedVis.params.controls; - controls.forEach((control: Record, i: number) => { - if (!control.indexPattern) { - return; - } - control.indexPatternRefName = `control_${i}_index_pattern`; - references.push({ - name: control.indexPatternRefName, - type: 'index-pattern', - id: control.indexPattern, - }); - }); + const { type, params } = state.savedVis ?? {}; + + if (type && params) { + extractControlsReferences(type, params, references, `control_${state.id}`); + extractTimeSeriesReferences(type, params, references, `metrics_${state.id}`); } return { state: _state, references }; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts new file mode 100644 index 0000000000000..d116fd2e2e9a7 --- /dev/null +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { SavedObjectReference } from '../../../../../core/types'; +import { VisParams } from '../../../common'; + +const isControlsVis = (visType: string) => visType === 'input_control_vis'; + +export const extractControlsReferences = ( + visType: string, + visParams: VisParams, + references: SavedObjectReference[] = [], + prefix: string = 'control' +) => { + if (isControlsVis(visType)) { + (visParams?.controls ?? []).forEach((control: Record, i: number) => { + if (!control.indexPattern) { + return; + } + control.indexPatternRefName = `${prefix}_${i}_index_pattern`; + references.push({ + name: control.indexPatternRefName, + type: 'index-pattern', + id: control.indexPattern, + }); + delete control.indexPattern; + }); + } +}; + +export const injectControlsReferences = ( + visType: string, + visParams: VisParams, + references: SavedObjectReference[] +) => { + if (isControlsVis(visType)) { + (visParams.controls ?? []).forEach((control: Record) => { + if (!control.indexPatternRefName) { + return; + } + const reference = references.find((ref) => ref.name === control.indexPatternRefName); + if (!reference) { + throw new Error(`Could not find index pattern reference "${control.indexPatternRefName}"`); + } + control.indexPattern = reference.id; + delete control.indexPatternRefName; + }); + } +}; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/index.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/index.ts new file mode 100644 index 0000000000000..0acda1c0a0f80 --- /dev/null +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { extractControlsReferences, injectControlsReferences } from './controls_references'; +export { extractTimeSeriesReferences, injectTimeSeriesReferences } from './timeseries_references'; + +export { extractReferences, injectReferences } from './saved_visualization_references'; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts similarity index 69% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts rename to src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts index f81054febcc44..867febd2544b0 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts @@ -7,8 +7,8 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; -import { VisSavedObject } from '../types'; -import { SavedVisState } from '../../common'; +import { VisSavedObject } from '../../types'; +import { SavedVisState } from '../../../common'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { @@ -21,13 +21,13 @@ describe('extractReferences', () => { }; const updatedDoc = extractReferences(doc); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - }, - "references": Array [], -} -`); + Object { + "attributes": Object { + "foo": true, + }, + "references": Array [], + } + `); }); test('extracts references from savedSearchId', () => { @@ -41,20 +41,20 @@ Object { }; const updatedDoc = extractReferences(doc); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "savedSearchRefName": "search_0", - }, - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "savedSearchRefName": "search_0", + }, + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + } + `); }); test('extracts references from controls', () => { @@ -63,6 +63,7 @@ Object { attributes: { foo: true, visState: JSON.stringify({ + type: 'input_control_vis', params: { controls: [ { @@ -81,20 +82,20 @@ Object { const updatedDoc = extractReferences(doc); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "visState": "{\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"bar\\":false}]}}", - }, - "references": Array [ - Object { - "id": "pattern*", - "name": "control_0_index_pattern", - "type": "index-pattern", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "visState": "{\\"type\\":\\"input_control_vis\\",\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"bar\\":false}]}}", + }, + "references": Array [ + Object { + "id": "pattern*", + "name": "control_0_index_pattern", + "type": "index-pattern", + }, + ], + } + `); }); }); @@ -106,11 +107,11 @@ describe('injectReferences', () => { } as VisSavedObject; injectReferences(context, []); expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "title": "test", -} -`); + Object { + "id": "1", + "title": "test", + } + `); }); test('injects references into context', () => { @@ -119,6 +120,7 @@ Object { title: 'test', savedSearchRefName: 'search_0', visState: ({ + type: 'input_control_vis', params: { controls: [ { @@ -146,25 +148,26 @@ Object { ]; injectReferences(context, references); expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "savedSearchId": "123", - "title": "test", - "visState": Object { - "params": Object { - "controls": Array [ - Object { - "foo": true, - "indexPattern": "pattern*", - }, - Object { - "foo": false, + Object { + "id": "1", + "savedSearchId": "123", + "title": "test", + "visState": Object { + "params": Object { + "controls": Array [ + Object { + "foo": true, + "indexPattern": "pattern*", + }, + Object { + "foo": false, + }, + ], + }, + "type": "input_control_vis", }, - ], - }, - }, -} -`); + } + `); }); test(`fails when it can't find the saved search reference in the array`, () => { @@ -183,6 +186,7 @@ Object { id: '1', title: 'test', visState: ({ + type: 'input_control_vis', params: { controls: [ { diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.ts similarity index 67% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts rename to src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.ts index 27b5a4542036b..6a4f9812db971 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.ts @@ -10,13 +10,16 @@ import { SavedObjectAttribute, SavedObjectAttributes, SavedObjectReference, -} from '../../../../core/public'; -import { VisSavedObject } from '../types'; +} from '../../../../../core/public'; +import { SavedVisState, VisSavedObject } from '../../types'; import { extractSearchSourceReferences, injectSearchSourceReferences, SearchSourceFields, -} from '../../../data/public'; +} from '../../../../data/public'; + +import { extractTimeSeriesReferences, injectTimeSeriesReferences } from './timeseries_references'; +import { extractControlsReferences, injectControlsReferences } from './controls_references'; export function extractReferences({ attributes, @@ -49,20 +52,13 @@ export function extractReferences({ // Extract index patterns from controls if (updatedAttributes.visState) { - const visState = JSON.parse(String(updatedAttributes.visState)); - const controls = (visState.params && visState.params.controls) || []; - controls.forEach((control: Record, i: number) => { - if (!control.indexPattern) { - return; - } - control.indexPatternRefName = `control_${i}_index_pattern`; - updatedReferences.push({ - name: control.indexPatternRefName, - type: 'index-pattern', - id: control.indexPattern, - }); - delete control.indexPattern; - }); + const visState = JSON.parse(String(updatedAttributes.visState)) as SavedVisState; + + if (visState.type && visState.params) { + extractControlsReferences(visState.type, visState.params, updatedReferences); + extractTimeSeriesReferences(visState.type, visState.params, updatedReferences); + } + updatedAttributes.visState = JSON.stringify(visState); } @@ -89,18 +85,11 @@ export function injectReferences(savedObject: VisSavedObject, references: SavedO savedObject.savedSearchId = savedSearchReference.id; delete savedObject.savedSearchRefName; } - if (savedObject.visState) { - const controls = (savedObject.visState.params && savedObject.visState.params.controls) || []; - controls.forEach((control: Record) => { - if (!control.indexPatternRefName) { - return; - } - const reference = references.find((ref) => ref.name === control.indexPatternRefName); - if (!reference) { - throw new Error(`Could not find index pattern reference "${control.indexPatternRefName}"`); - } - control.indexPattern = reference.id; - delete control.indexPatternRefName; - }); + + const { type, params } = savedObject.visState ?? {}; + + if (type && params) { + injectControlsReferences(type, params, references); + injectTimeSeriesReferences(type, params, references); } } diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts new file mode 100644 index 0000000000000..57706ee824e8d --- /dev/null +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts @@ -0,0 +1,83 @@ +/* + * Copyright 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 { SavedObjectReference } from '../../../../../core/types'; +import { VisParams } from '../../../common'; + +/** @internal **/ +const REF_NAME_POSTFIX = '_ref_name'; + +/** @internal **/ +const INDEX_PATTERN_REF_TYPE = 'index_pattern'; + +/** @internal **/ +type Action = (object: Record, key: string) => void; + +const isMetricsVis = (visType: string) => visType === 'metrics'; + +const doForExtractedIndices = (action: Action, visParams: VisParams) => { + action(visParams, 'index_pattern'); + + visParams.series.forEach((series: any) => { + if (series.override_index_pattern) { + action(series, 'series_index_pattern'); + } + }); + + if (visParams.annotations) { + visParams.annotations.forEach((annotation: any) => { + action(annotation, 'index_pattern'); + }); + } +}; + +export const extractTimeSeriesReferences = ( + visType: string, + visParams: VisParams, + references: SavedObjectReference[] = [], + prefix: string = 'metrics' +) => { + let i = 0; + if (isMetricsVis(visType)) { + doForExtractedIndices((object, key) => { + if (object[key] && object[key].id) { + const name = `${prefix}_${i++}_index_pattern`; + + object[key + REF_NAME_POSTFIX] = name; + references.push({ + name, + type: INDEX_PATTERN_REF_TYPE, + id: object[key].id, + }); + delete object[key]; + } + }, visParams); + } +}; + +export const injectTimeSeriesReferences = ( + visType: string, + visParams: VisParams, + references: SavedObjectReference[] +) => { + if (isMetricsVis(visType)) { + doForExtractedIndices((object, key) => { + const refKey = key + REF_NAME_POSTFIX; + + if (object[refKey]) { + const refValue = references.find((ref) => ref.name === object[refKey]); + + if (refValue) { + object[key] = { id: refValue.id }; + } + + delete object[refKey]; + } + }, visParams); + } +}; diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index 6f1fba26b39b3..e7410c7a97343 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1921,4 +1921,36 @@ describe('migration visualization', () => { expect(migratedTestDoc).toEqual(expectedDoc); }); }); + + describe('7.13.0 tsvb hide Last value indicator by default', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.13.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const createTestDocWithType = (type: string) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{"type":"metrics","params":{"type":"${type}"}}`, + }, + }); + + it('should set hide_last_value_indicator param to true', () => { + const migratedTestDoc = migrate(createTestDocWithType('markdown')); + const hideLastValueIndicator = JSON.parse(migratedTestDoc.attributes.visState).params + .hide_last_value_indicator; + + expect(hideLastValueIndicator).toBeTruthy(); + }); + + it('should ignore timeseries type', () => { + const migratedTestDoc = migrate(createTestDocWithType('timeseries')); + const hideLastValueIndicator = JSON.parse(migratedTestDoc.attributes.visState).params + .hide_last_value_indicator; + + expect(hideLastValueIndicator).toBeUndefined(); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index afb59266d0dbf..633442ec55d69 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -790,6 +790,35 @@ const removeTSVBSearchSource: SavedObjectMigrationFn = (doc) => { return doc; }; +const addSupportOfDualIndexSelectionModeInTSVB: 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; + + if (typeof params?.index_pattern === 'string') { + params.use_kibana_indexes = false; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + // [Data table visualization] Enable toolbar by default const enableDataTableVisToolbar: SavedObjectMigrationFn = (doc) => { let visState; @@ -894,6 +923,34 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => return doc; }; +/** + * [TSVB] Hide Last value indicator by default for all TSVB types except timeseries + */ +const hideTSVBLastValueIndicator: SavedObjectMigrationFn = (doc) => { + try { + const visState = JSON.parse(doc.attributes.visState); + + if (visState && visState.type === 'metrics' && visState.params.type !== 'timeseries') + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify({ + ...visState, + params: { + ...visState.params, + hide_last_value_indicator: true, + }, + }), + }, + }; + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + + 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 @@ -929,4 +986,5 @@ 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), }; diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts index 164d5b5fa72ac..89e1e7f03e149 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts @@ -61,7 +61,7 @@ export async function getStats( // `map` to get the raw types const visSummaries: VisSummary[] = esResponse.hits.hits.map((hit) => { - const spacePhrases = hit._id.split(':'); + const spacePhrases = hit._id.toString().split(':'); const lastUpdated: string = get(hit, '_source.updated_at'); const space = spacePhrases.length === 3 ? spacePhrases[0] : 'default'; // if in a custom space, the format of a saved object ID is space:type:id const visualization = get(hit, '_source.visualization', { visState: '{}' }); diff --git a/src/setup_node_env/ensure_node_preserve_symlinks.js b/src/setup_node_env/ensure_node_preserve_symlinks.js index 0d72ec85e6c87..826244c4829fc 100644 --- a/src/setup_node_env/ensure_node_preserve_symlinks.js +++ b/src/setup_node_env/ensure_node_preserve_symlinks.js @@ -9,10 +9,51 @@ (function () { var cp = require('child_process'); + var calculateInspectPortOnExecArgv = function (processExecArgv) { + var execArgv = [].concat(processExecArgv); + + if (execArgv.length === 0) { + return execArgv; + } + + var inspectFlagIndex = execArgv.reverse().findIndex(function (flag) { + return flag.startsWith('--inspect'); + }); + + if (inspectFlagIndex !== -1) { + var inspectFlag; + var inspectPortCounter = 9230; + var argv = execArgv[inspectFlagIndex]; + + if (argv.includes('=')) { + // --inspect=port + var argvSplit = argv.split('='); + var flag = argvSplit[0]; + var port = argvSplit[1]; + inspectFlag = flag; + inspectPortCounter = Number.parseInt(port, 10) + 1; + } else { + // --inspect + inspectFlag = argv; + + // is number? + if (String(execArgv[inspectFlagIndex + 1]).match(/^[0-9]+$/)) { + // --inspect port + inspectPortCounter = Number.parseInt(execArgv[inspectFlagIndex + 1], 10) + 1; + execArgv.slice(inspectFlagIndex + 1, 1); + } + } + + execArgv[inspectFlagIndex] = inspectFlag + '=' + inspectPortCounter; + } + + return execArgv; + }; + var preserveSymlinksOption = '--preserve-symlinks'; var preserveSymlinksMainOption = '--preserve-symlinks-main'; var nodeOptions = (process && process.env && process.env.NODE_OPTIONS) || []; - var nodeExecArgv = (process && process.execArgv) || []; + var nodeExecArgv = calculateInspectPortOnExecArgv((process && process.execArgv) || []); var isPreserveSymlinksPresent = nodeOptions.includes(preserveSymlinksOption) || nodeExecArgv.includes(preserveSymlinksOption); diff --git a/src/setup_node_env/index.js b/src/setup_node_env/index.js index 08664344db393..9ce60766997cc 100644 --- a/src/setup_node_env/index.js +++ b/src/setup_node_env/index.js @@ -7,4 +7,5 @@ */ require('./no_transpilation'); +// eslint-disable-next-line import/no-extraneous-dependencies require('@kbn/optimizer').registerNodeAutoTranspilation(); diff --git a/test/api_integration/apis/console/index.ts b/test/api_integration/apis/console/index.ts new file mode 100644 index 0000000000000..ad4f8256f97ad --- /dev/null +++ b/test/api_integration/apis/console/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('core', () => { + loadTestFile(require.resolve('./proxy_route')); + }); +} diff --git a/test/api_integration/apis/console/proxy_route.ts b/test/api_integration/apis/console/proxy_route.ts new file mode 100644 index 0000000000000..d8a5f57a41a6e --- /dev/null +++ b/test/api_integration/apis/console/proxy_route.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('POST /api/console/proxy', () => { + describe('system indices behavior', () => { + it('returns warning header when making requests to .kibana index', async () => { + return await supertest + .post('/api/console/proxy?method=GET&path=/.kibana/_settings') + .set('kbn-xsrf', 'true') + .then((response) => { + expect(response.header).to.have.property('warning'); + const { warning } = response.header as { warning: string }; + expect(warning.startsWith('299')).to.be(true); + expect(warning.includes('system indices')).to.be(true); + }); + }); + + it('does not forward x-elastic-product-origin', async () => { + // If we pass the header and we still get the warning back, we assume that the header was not forwarded. + return await supertest + .post('/api/console/proxy?method=GET&path=/.kibana/_settings') + .set('kbn-xsrf', 'true') + .set('x-elastic-product-origin', 'kibana') + .then((response) => { + expect(response.header).to.have.property('warning'); + const { warning } = response.header as { warning: string }; + expect(warning.startsWith('299')).to.be(true); + expect(warning.includes('system indices')).to.be(true); + }); + }); + }); + }); +} diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index b889b59fdaf32..99327901ec8c3 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -48,12 +48,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('should load elasticsearch index containing sample data with dates relative to current time', async () => { - const { body: resp } = await es.search({ + const { body: resp } = await es.search<{ timestamp: string }>({ index: 'kibana_sample_data_flights', }); const doc = resp.hits.hits[0]; - const docMilliseconds = Date.parse(doc._source.timestamp); + const docMilliseconds = Date.parse(doc._source!.timestamp); const nowMilliseconds = Date.now(); const delta = Math.abs(nowMilliseconds - docMilliseconds); expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 4); @@ -66,12 +66,12 @@ export default function ({ getService }: FtrProviderContext) { .post(`/api/sample_data/flights?now=${nowString}`) .set('kbn-xsrf', 'kibana'); - const { body: resp } = await es.search({ + const { body: resp } = await es.search<{ timestamp: string }>({ index: 'kibana_sample_data_flights', }); const doc = resp.hits.hits[0]; - const docMilliseconds = Date.parse(doc._source.timestamp); + const docMilliseconds = Date.parse(doc._source!.timestamp); const nowMilliseconds = Date.parse(nowString); const delta = Math.abs(nowMilliseconds - docMilliseconds); expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 4); diff --git a/test/api_integration/apis/index.ts b/test/api_integration/apis/index.ts index 33495ad2c604b..0d87569cb8b97 100644 --- a/test/api_integration/apis/index.ts +++ b/test/api_integration/apis/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', () => { + loadTestFile(require.resolve('./console')); loadTestFile(require.resolve('./core')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index 1f1f1a5c98cd6..87997ab4231a2 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -15,7 +15,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import expect from '@kbn/expect'; import { ElasticsearchClient, SavedObjectsType } from 'src/core/server'; -import { SearchResponse } from '../../../../src/core/server/elasticsearch/client'; + import { DocumentMigrator, IndexMigrator, @@ -113,7 +113,7 @@ export default ({ getService }: FtrProviderContext) => { await esClient.indices.putTemplate({ name: 'migration_test_a_template', body: { - index_patterns: 'migration_test_a', + index_patterns: ['migration_test_a'], mappings: { dynamic: 'strict', properties: { baz: { type: 'text' } }, @@ -125,7 +125,7 @@ export default ({ getService }: FtrProviderContext) => { await esClient.indices.putTemplate({ name: 'migration_a_template', body: { - index_patterns: index, + index_patterns: [index], mappings: { dynamic: 'strict', properties: { baz: { type: 'text' } }, @@ -744,7 +744,7 @@ async function migrateIndex({ } async function fetchDocs(esClient: ElasticsearchClient, index: string) { - const { body } = await esClient.search>({ index }); + const { body } = await esClient.search({ index }); return body.hits.hits .map((h) => ({ diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index a7b4da566b143..d0a09ee58d335 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -156,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) { describe('application usage limits', () => { function createSavedObject(viewId?: string) { return supertest - .post('/api/saved_objects/application_usage_transactional') + .post('/api/saved_objects/application_usage_daily') .send({ attributes: { appId: 'test-app', @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { await Promise.all( savedObjectIds.map((savedObjectId) => { return supertest - .delete(`/api/saved_objects/application_usage_transactional/${savedObjectId}`) + .delete(`/api/saved_objects/application_usage_daily/${savedObjectId}`) .expect(200); }) ); @@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/saved_objects/_bulk_create') .send( new Array(10001).fill(0).map(() => ({ - type: 'application_usage_transactional', + type: 'application_usage_daily', attributes: { appId: 'test-app', minutesOnScreen: 1, @@ -248,13 +248,12 @@ export default function ({ getService }: FtrProviderContext) { // The SavedObjects API does not allow bulk deleting, and deleting one by one takes ages and the tests timeout await es.deleteByQuery({ index: '.kibana', - body: { query: { term: { type: 'application_usage_transactional' } } }, + body: { query: { term: { type: 'application_usage_daily' } } }, conflicts: 'proceed', }); }); - // flaky https://github.com/elastic/kibana/issues/94513 - it.skip("should only use the first 10k docs for the application_usage data (they'll be rolled up in a later process)", async () => { + it("should only use the first 10k docs for the application_usage data (they'll be rolled up in a later process)", async () => { const stats = await retrieveTelemetry(supertest); expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ 'test-app': { diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts index 2c58794c96eca..a76d09481eca1 100644 --- a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -26,15 +26,13 @@ export default function optInTest({ getService }: FtrProviderContext) { await supertest.put('/api/telemetry/v2/userHasSeenNotice').set('kbn-xsrf', 'xxx').expect(200); const { - body: { - _source: { telemetry }, - }, - } = await client.get({ + body: { _source }, + } = await client.get<{ telemetry: { userHasSeenNotice: boolean } }>({ index: '.kibana', id: 'telemetry:telemetry', }); - expect(telemetry.userHasSeenNotice).to.be(true); + expect(_source?.telemetry.userHasSeenNotice).to.be(true); }); }); } diff --git a/test/api_integration/apis/ui_metric/ui_metric.ts b/test/api_integration/apis/ui_metric/ui_metric.ts index 99007376e1ea4..47d10da9a1b29 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.ts +++ b/test/api_integration/apis/ui_metric/ui_metric.ts @@ -102,12 +102,12 @@ export default function ({ getService }: FtrProviderContext) { body: { hits: { hits }, }, - } = await es.search({ index: '.kibana', q: 'type:ui-metric' }); + } = await es.search({ index: '.kibana', q: 'type:ui-metric' }); const countTypeEvent = hits.find( (hit: { _id: string }) => hit._id === `ui-metric:myApp:${uniqueEventName}` ); - expect(countTypeEvent._source['ui-metric'].count).to.eql(3); + expect(countTypeEvent?._source['ui-metric'].count).to.eql(3); }); }); } diff --git a/test/common/services/elasticsearch.ts b/test/common/services/elasticsearch.ts index 99335f8405828..7b8ff6bd6c8f4 100644 --- a/test/common/services/elasticsearch.ts +++ b/test/common/services/elasticsearch.ts @@ -10,10 +10,14 @@ import { format as formatUrl } from 'url'; import fs from 'fs'; import { Client } from '@elastic/elasticsearch'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { FtrProviderContext } from '../ftr_provider_context'; -export function ElasticsearchProvider({ getService }: FtrProviderContext) { +/* + registers Kibana-specific @elastic/elasticsearch client instance. + */ +export function ElasticsearchProvider({ getService }: FtrProviderContext): KibanaClient { const config = getService('config'); if (process.env.TEST_CLOUD) { diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index aeb02e5c30eb8..cc62608fbde6d 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -105,24 +105,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should modify the time range when the histogram is brushed', async function () { // this is the number of renderings of the histogram needed when new data is fetched // this needs to be improved - const renderingCountInc = 3; + const renderingCountInc = 1; const prevRenderingCount = await elasticChart.getVisualizationRenderingCount(); await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); await retry.waitFor('chart rendering complete', async () => { - const actualRenderingCount = await elasticChart.getVisualizationRenderingCount(); - log.debug(`Number of renderings before brushing: ${actualRenderingCount}`); - return actualRenderingCount === prevRenderingCount + renderingCountInc; + const actualCount = await elasticChart.getVisualizationRenderingCount(); + const expectedCount = prevRenderingCount + renderingCountInc; + log.debug( + `renderings before brushing - actual: ${actualCount} expected: ${expectedCount}` + ); + return actualCount === expectedCount; }); await PageObjects.discover.brushHistogram(); await PageObjects.discover.waitUntilSearchingHasFinished(); await retry.waitFor('chart rendering complete after being brushed', async () => { - const actualRenderingCount = await elasticChart.getVisualizationRenderingCount(); - log.debug(`Number of renderings after brushing: ${actualRenderingCount}`); - return actualRenderingCount === prevRenderingCount + 6; + const actualCount = await elasticChart.getVisualizationRenderingCount(); + const expectedCount = prevRenderingCount + renderingCountInc * 2; + log.debug( + `renderings after brushing - actual: ${actualCount} expected: ${expectedCount}` + ); + return actualCount === expectedCount; }); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(Math.round(newDurationHours)).to.be(26); + expect(Math.round(newDurationHours)).to.be(27); await retry.waitFor('doc table to contain the right search result', async () => { const rowData = await PageObjects.discover.getDocTableField(1); diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 2a6096f8d1a78..72deb74459ab9 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -22,7 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); - describe('discover histogram', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/94532 + describe.skip('discover histogram', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 3febeb06fd600..edcb002000183 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -65,6 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const finalRows = await PageObjects.discover.getDocTableRows(); expect(finalRows.length).to.be.above(initialRows.length); expect(finalRows.length).to.be(rowsHardLimit); + await PageObjects.discover.backToTop(); }); it('should go the end of the table when using the accessible Skip button', async function () { @@ -74,6 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const footer = await PageObjects.discover.getDocTableFooter(); log.debug(await footer.getVisibleText()); expect(await footer.getVisibleText()).to.have.string(rowsHardLimit); + await PageObjects.discover.backToTop(); }); describe('expand a document row', function () { diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts new file mode 100644 index 0000000000000..8cb39feb2e6bb --- /dev/null +++ b/test/functional/apps/discover/_huge_fields.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + + describe('test large number of fields in sidebar', function () { + before(async function () { + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); + await esArchiver.loadIfNeeded('large_fields'); + await PageObjects.settings.navigateTo(); + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`, + }); + await PageObjects.settings.createIndexPattern('*huge*', 'date', true); + await PageObjects.common.navigateToApp('discover'); + }); + + it('test_huge data should have expected number of fields', async function () { + await PageObjects.discover.selectIndexPattern('*huge*'); + // initially this field should not be rendered + const fieldExistsBeforeScrolling = await testSubjects.exists('field-myvar1050'); + expect(fieldExistsBeforeScrolling).to.be(false); + // scrolling down a little, should render this field + await testSubjects.scrollIntoView('fieldToggle-myvar1029'); + const fieldExistsAfterScrolling = await testSubjects.exists('field-myvar1050'); + expect(fieldExistsAfterScrolling).to.be(true); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + await esArchiver.unload('large_fields'); + await kibanaServer.uiSettings.replace({}); + }); + }); +} diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index d91507e9e442c..9726b097c8f62 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,7 +26,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/89477 describe('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); @@ -120,6 +119,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('does not allow saving a query with a non-unique name', async () => { + // this check allows this test to run stand alone, also should fix occacional flakiness + const savedQueryExists = await savedQueryManagementComponent.savedQueryExist('OkResponse'); + if (!savedQueryExists) { + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + } await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse'); }); diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index 5c319312c8137..e526cdaccbd4c 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -47,5 +47,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_data_grid_doc_navigation')); loadTestFile(require.resolve('./_data_grid_doc_table')); loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); + loadTestFile(require.resolve('./_huge_fields')); }); } diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index 4b3533f20c8dc..e3ff1819aed13 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); + const testSubjects = getService('testSubjects'); describe('runtime fields', function () { this.tags(['skipFirefox']); @@ -47,6 +48,20 @@ export default function ({ getService, getPageObjects }) { expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1); }); }); + + it('should modify runtime field', async function () { + await PageObjects.settings.filterField(fieldName); + await testSubjects.click('editFieldFormat'); + await PageObjects.settings.setFieldType('Long'); + await PageObjects.settings.changeFieldScript('emit(6);'); + await PageObjects.settings.clickSaveField(); + await PageObjects.settings.confirmSave(); + }); + + it('should delete runtime field', async function () { + await testSubjects.click('deleteField'); + await PageObjects.settings.confirmDelete(); + }); }); }); } diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/_heatmap_chart.ts index 660f45179631e..79a9a6cbd5aca 100644 --- a/test/functional/apps/visualize/_heatmap_chart.ts +++ b/test/functional/apps/visualize/_heatmap_chart.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); - describe('heatmap chart', function indexPatternCreation() { + // FLAKY: https://github.com/elastic/kibana/issues/95642 + describe.skip('heatmap chart', function indexPatternCreation() { const vizName1 = 'Visualization HeatmapChart'; before(async function () { diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index ba500904d75c7..6b0080c3856fd 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -43,6 +43,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.clickPanelOptions('metric'); + await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value'); + await PageObjects.visualBuilder.clickDataTab('metric'); }); it('should not have inspector enabled', async () => { @@ -81,12 +84,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.checkGaugeTabIsPresent(); }); + it('should "Entire time range" selected as timerange mode for new visualization', async () => { + await PageObjects.visualBuilder.clickPanelOptions('gauge'); + await PageObjects.visualBuilder.checkSelectedDataTimerangeMode('Entire time range'); + await PageObjects.visualBuilder.clickDataTab('gauge'); + }); + it('should verify gauge label and count display', async () => { await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const labelString = await PageObjects.visualBuilder.getGaugeLabel(); expect(labelString).to.be('Count'); const gaugeCount = await PageObjects.visualBuilder.getGaugeCount(); - expect(gaugeCount).to.be('156'); + expect(gaugeCount).to.be('13,830'); }); }); @@ -95,6 +104,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickTopN(); await PageObjects.visualBuilder.checkTopNTabIsPresent(); + await PageObjects.visualBuilder.clickPanelOptions('topN'); + await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value'); + await PageObjects.visualBuilder.clickDataTab('topN'); }); it('should verify topN label and count display', async () => { @@ -107,33 +119,51 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('switch index patterns', () => { + before(async () => { + await esArchiver.loadIfNeeded('index_pattern_without_timefield'); + }); + beforeEach(async () => { - log.debug('Load kibana_sample_data_flights data'); - await esArchiver.loadIfNeeded('kibana_sample_data_flights'); await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.clickPanelOptions('metric'); + await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value'); + await PageObjects.visualBuilder.clickDataTab('metric'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2019 @ 00:00:00.000', + 'Sep 23, 2019 @ 00:00:00.000' + ); }); + after(async () => { await security.testUser.restoreDefaults(); - await esArchiver.unload('kibana_sample_data_flights'); + await esArchiver.unload('index_pattern_without_timefield'); }); - it('should be able to switch between index patterns', async () => { - const value = await PageObjects.visualBuilder.getMetricValue(); - expect(value).to.eql('156'); + const switchIndexTest = async (useKibanaIndexes: boolean) => { await PageObjects.visualBuilder.clickPanelOptions('metric'); - const fromTime = 'Oct 22, 2018 @ 00:00:00.000'; - const toTime = 'Oct 28, 2018 @ 23:59:59.999'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.visualBuilder.setIndexPatternValue('', false); + + const value = await PageObjects.visualBuilder.getMetricValue(); + expect(value).to.eql('0'); + // Sometimes popovers take some time to appear in Firefox (#71979) await retry.tryForTime(20000, async () => { - await PageObjects.visualBuilder.setIndexPatternValue('kibana_sample_data_flights'); + await PageObjects.visualBuilder.setIndexPatternValue('with-timefield', useKibanaIndexes); await PageObjects.visualBuilder.waitForIndexPatternTimeFieldOptionsLoaded(); await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp'); }); const newValue = await PageObjects.visualBuilder.getMetricValue(); - expect(newValue).to.eql('18'); + expect(newValue).to.eql('1'); + }; + + it('should be able to switch using text mode selection', async () => { + await switchIndexTest(false); + }); + + it('should be able to switch combo box mode selection', async () => { + await switchIndexTest(true); }); }); diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index caf9cab8b703a..b61fbf967a9bd 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -37,6 +37,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'Sep 22, 2015 @ 06:00:00.000', 'Sep 22, 2015 @ 11:00:00.000' ); + await visualBuilder.markdownSwitchSubTab('options'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.markdownSwitchSubTab('markdown'); }); it('should render subtabs and table variables markdown components', async () => { diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index dfa232b6e527d..36c0e26430ff5 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -24,6 +24,9 @@ export default function ({ getPageObjects }: FtrProviderContext) { await visualBuilder.clickTable(); await visualBuilder.checkTableTabIsPresent(); + await visualBuilder.clickPanelOptions('table'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.clickDataTab('table'); await visualBuilder.selectGroupByField('machine.os.raw'); await visualBuilder.setColumnLabelValue('OS'); await visChart.waitForVisualizationRenderingStabilized(); diff --git a/test/functional/fixtures/es_archiver/deprecations_service/data.json b/test/functional/fixtures/es_archiver/deprecations_service/data.json new file mode 100644 index 0000000000000..31ce5af20b46c --- /dev/null +++ b/test/functional/fixtures/es_archiver/deprecations_service/data.json @@ -0,0 +1,14 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "test-deprecations-plugin:ff3733a0-9fty-11e7-ahb3-3dcb94193fab", + "source": { + "type": "test-deprecations-plugin", + "updated_at": "2021-02-11T18:51:23.794Z", + "test-deprecations-plugin": { + "title": "Test saved object" + } + } + } +} diff --git a/test/functional/fixtures/es_archiver/deprecations_service/mappings.json b/test/functional/fixtures/es_archiver/deprecations_service/mappings.json new file mode 100644 index 0000000000000..5f7c7e0e7b7dc --- /dev/null +++ b/test/functional/fixtures/es_archiver/deprecations_service/mappings.json @@ -0,0 +1,289 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "query": { + "properties": { + "title": { + "type": "text" + }, + "description": { + "type": "text" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "type": "keyword", + "index": false + } + } + }, + "filters": { + "type": "object", + "enabled": false + }, + "timefilter": { + "type": "object", + "enabled": false + } + } + }, + "test-deprecations-plugin": { + "properties": { + "title": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index c6412f55dffbf..6d9641a1a920e 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -463,6 +463,21 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async getWelcomeText() { return await testSubjects.getVisibleText('global-banner-item'); } + + /** + * Clicks on an element, and validates that the desired effect has taken place + * by confirming the existence of a validator + */ + async clickAndValidate( + clickTarget: string, + validator: string, + isValidatorCssString: boolean = false, + topOffset?: number + ) { + await testSubjects.click(clickTarget, undefined, topOffset); + const validate = isValidatorCssString ? find.byCssSelector : testSubjects.exists; + await validate(validator); + } } return new CommonPage(); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 733f5cb59fbbb..32288239f9848 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -210,6 +210,15 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return skipButton.click(); } + /** + * When scrolling down the legacy table there's a link to scroll up + * So this is done by this function + */ + public async backToTop() { + const skipButton = await testSubjects.find('discoverBackToTop'); + return skipButton.click(); + } + public async getDocTableFooter() { return await testSubjects.find('discoverDocTableFooter'); } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 4151a8c1a1893..14bd002ec9487 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -502,6 +502,16 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await this.closeIndexPatternFieldEditor(); } + public async confirmSave() { + await testSubjects.setValue('saveModalConfirmText', 'change'); + await testSubjects.click('confirmModalConfirmButton'); + } + + public async confirmDelete() { + await testSubjects.setValue('deleteModalConfirmText', 'remove'); + await testSubjects.click('confirmModalConfirmButton'); + } + async closeIndexPatternFieldEditor() { await retry.waitFor('field editor flyout to close', async () => { return !(await testSubjects.exists('euiFlyoutCloseButton')); @@ -543,6 +553,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider browser.pressKeys(script); } + async changeFieldScript(script: string) { + log.debug('set script = ' + script); + const formatRow = await testSubjects.find('valueRow'); + const getMonacoTextArea = async () => (await formatRow.findAllByCssSelector('textarea'))[0]; + retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea())); + const monacoTextArea = await getMonacoTextArea(); + await monacoTextArea.focus(); + browser.pressKeys(browser.keys.DELETE.repeat(30)); + browser.pressKeys(script); + } + async clickAddScriptedField() { log.debug('click Add Scripted Field'); await testSubjects.click('addScriptedFieldLink'); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index d7bb84394ae3c..3ed5d74808fce 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -154,7 +154,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async getMarkdownText(): Promise { - const el = await find.byCssSelector('.tvbEditorVisualization'); + const el = await find.byCssSelector('.tvbVis'); const text = await el.getVisibleText(); return text; } @@ -431,10 +431,39 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro await PageObjects.header.waitUntilLoadingHasFinished(); } - public async setIndexPatternValue(value: string) { - const el = await testSubjects.find('metricsIndexPatternInput'); - await el.clearValue(); - await el.type(value, { charByChar: true }); + public async clickDataTab(tabName: string) { + await testSubjects.click(`${tabName}EditorDataBtn`); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + + public async switchIndexPatternSelectionMode(useKibanaIndices: boolean) { + await testSubjects.click('switchIndexPatternSelectionModePopover'); + await testSubjects.setEuiSwitch( + 'switchIndexPatternSelectionMode', + useKibanaIndices ? 'check' : 'uncheck' + ); + } + + public async setIndexPatternValue(value: string, useKibanaIndices?: boolean) { + const metricsIndexPatternInput = 'metricsIndexPatternInput'; + + if (useKibanaIndices !== undefined) { + await this.switchIndexPatternSelectionMode(useKibanaIndices); + } + + if (useKibanaIndices === false) { + const el = await testSubjects.find(metricsIndexPatternInput); + await el.clearValue(); + if (value) { + await el.type(value, { charByChar: true }); + } + } else { + await comboBox.clearInputField(metricsIndexPatternInput); + if (value) { + await comboBox.setCustom(metricsIndexPatternInput, value); + } + } + await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -614,6 +643,16 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro ); return await comboBox.isOptionSelected(groupBy, value); } + + public async setMetricsDataTimerangeMode(value: string) { + const dataTimeRangeMode = await testSubjects.find('dataTimeRangeMode'); + return await comboBox.setElement(dataTimeRangeMode, value); + } + + public async checkSelectedDataTimerangeMode(value: string) { + const dataTimeRangeMode = await testSubjects.find('dataTimeRangeMode'); + return await comboBox.isOptionSelected(dataTimeRangeMode, value); + } } return new VisualBuilderPage(); diff --git a/test/functional/screenshots/baseline/tsvb_dashboard.png b/test/functional/screenshots/baseline/tsvb_dashboard.png index e0d79c7234f6a..4280199e77d11 100644 Binary files a/test/functional/screenshots/baseline/tsvb_dashboard.png and b/test/functional/screenshots/baseline/tsvb_dashboard.png differ diff --git a/test/functional/services/common/find.ts b/test/functional/services/common/find.ts index 2a86efad1ea9d..0cd4c14683f6e 100644 --- a/test/functional/services/common/find.ts +++ b/test/functional/services/common/find.ts @@ -79,11 +79,11 @@ export async function FindProvider({ getService }: FtrProviderContext) { return wrap(await driver.switchTo().activeElement()); } - public async setValue(selector: string, text: string): Promise { + public async setValue(selector: string, text: string, topOffset?: number): Promise { log.debug(`Find.setValue('${selector}', '${text}')`); return await retry.try(async () => { const element = await this.byCssSelector(selector); - await element.click(); + await element.click(topOffset); // in case the input element is actually a child of the testSubject, we // call clearValue() and type() on the element that is focused after @@ -413,14 +413,15 @@ export async function FindProvider({ getService }: FtrProviderContext) { public async clickByCssSelector( selector: string, - timeout: number = defaultFindTimeout + timeout: number = defaultFindTimeout, + topOffset?: number ): Promise { log.debug(`Find.clickByCssSelector('${selector}') with timeout=${timeout}`); await retry.try(async () => { const element = await this.byCssSelector(selector, timeout); if (element) { // await element.moveMouseTo(); - await element.click(); + await element.click(topOffset); } else { throw new Error(`Element with css='${selector}' is not found`); } diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index 28b37d9576e8c..111206ec9eafe 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -100,9 +100,13 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { await find.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { timeout }); } - public async click(selector: string, timeout: number = FIND_TIME): Promise { + public async click( + selector: string, + timeout: number = FIND_TIME, + topOffset?: number + ): Promise { log.debug(`TestSubjects.click(${selector})`); - await find.clickByCssSelector(testSubjSelector(selector), timeout); + await find.clickByCssSelector(testSubjSelector(selector), timeout, topOffset); } public async doubleClick(selector: string, timeout: number = FIND_TIME): Promise { @@ -187,12 +191,13 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { public async setValue( selector: string, text: string, - options: SetValueOptions = {} + options: SetValueOptions = {}, + topOffset?: number ): Promise { return await retry.try(async () => { const { clearWithKeyboard = false, typeCharByChar = false } = options; log.debug(`TestSubjects.setValue(${selector}, ${text})`); - await this.click(selector); + await this.click(selector, undefined, topOffset); // in case the input element is actually a child of the testSubject, we // call clearValue() and type() on the element that is focused after // clicking on the testSubject diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts index 5cd1f2c4f6202..7d6dad4f7858e 100644 --- a/test/functional/services/field_editor.ts +++ b/test/functional/services/field_editor.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function FieldEditorProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); - const retry = getService('retry'); const testSubjects = getService('testSubjects'); class FieldEditor { @@ -33,10 +32,17 @@ export function FieldEditorProvider({ getService }: FtrProviderContext) { await browser.pressKeys(script); } public async save() { - await retry.try(async () => { - await testSubjects.click('fieldSaveButton'); - await testSubjects.missingOrFail('fieldSaveButton', { timeout: 2000 }); - }); + await testSubjects.click('fieldSaveButton'); + } + + public async confirmSave() { + await testSubjects.setValue('saveModalConfirmText', 'change'); + await testSubjects.click('confirmModalConfirmButton'); + } + + public async confirmDelete() { + await testSubjects.setValue('deleteModalConfirmText', 'remove'); + await testSubjects.click('confirmModalConfirmButton'); } } diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 1a45aee877e1f..b1561b29342da 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -182,9 +182,9 @@ export class WebElementWrapper { * * @return {Promise} */ - public async click() { + public async click(topOffset?: number) { await this.retryCall(async function click(wrapper) { - await wrapper.scrollIntoViewIfNecessary(); + await wrapper.scrollIntoViewIfNecessary(topOffset); await wrapper._webElement.click(); }); } @@ -693,11 +693,11 @@ export class WebElementWrapper { * @nonstandard * @return {Promise} */ - public async scrollIntoViewIfNecessary(): Promise { + public async scrollIntoViewIfNecessary(topOffset?: number): Promise { await this.driver.executeScript( scrollIntoViewIfNecessary, this._webElement, - this.fixedHeaderHeight + topOffset || this.fixedHeaderHeight ); } diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index a39032af43295..7398e6ca8c12e 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -139,6 +139,13 @@ export function SavedQueryManagementComponentProvider({ await testSubjects.click('savedQueryFormSaveButton'); } + async savedQueryExist(title: string) { + await this.openSavedQueryManagementComponent(); + const exists = testSubjects.exists(`~load-saved-query-${title}-button`); + await this.closeSavedQueryManagementComponent(); + return exists; + } + async savedQueryExistOrFail(title: string) { await this.openSavedQueryManagementComponent(); await testSubjects.existOrFail(`~load-saved-query-${title}-button`); diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index fc747fcd71f17..1651e213ee82d 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -56,6 +56,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // Required to load new platform plugins via `--plugin-path` flag. '--env.name=development', + '--corePluginDeprecations.oldProperty=hello', + '--corePluginDeprecations.secret=100', + '--corePluginDeprecations.noLongerUsed=still_using', ...plugins.map( (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/kibana.json b/test/plugin_functional/plugins/core_plugin_deprecations/kibana.json new file mode 100644 index 0000000000000..bc251f97bea58 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "corePluginDeprecations", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["corePluginDeprecations"], + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/package.json b/test/plugin_functional/plugins/core_plugin_deprecations/package.json new file mode 100644 index 0000000000000..f14ec933f59b2 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/package.json @@ -0,0 +1,14 @@ +{ + "name": "core_plugin_deprecations", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_plugin_deprecations", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/public/application.tsx b/test/plugin_functional/plugins/core_plugin_deprecations/public/application.tsx new file mode 100644 index 0000000000000..e2166a249e34b --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/public/application.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 ReactDOM from 'react-dom'; +import { AppMountParameters } from 'kibana/public'; + +const DeprecationsApp = () =>
Deprcations App
; + +export const renderApp = ({ element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/public/index.ts b/test/plugin_functional/plugins/core_plugin_deprecations/public/index.ts new file mode 100644 index 0000000000000..bb6b3f0740b3b --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/public/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { PluginInitializer, PluginInitializerContext } from 'kibana/public'; +import { + CorePluginDeprecationsPlugin, + CorePluginDeprecationsPluginSetup, + CorePluginDeprecationsPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + CorePluginDeprecationsPluginSetup, + CorePluginDeprecationsPluginStart +> = (context: PluginInitializerContext) => new CorePluginDeprecationsPlugin(context); diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_deprecations/public/plugin.tsx new file mode 100644 index 0000000000000..bf807145e14bf --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/public/plugin.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/public'; + +declare global { + interface Window { + env?: PluginInitializerContext['env']; + } +} + +export class CorePluginDeprecationsPlugin + implements Plugin { + constructor(pluginContext: PluginInitializerContext) { + window.env = pluginContext.env; + } + public setup(core: CoreSetup) { + core.application.register({ + id: 'core-plugin-deprecations', + title: 'Core Plugin Deprecations', + async mount(params) { + const { renderApp } = await import('./application'); + await core.getStartServices(); + return renderApp(params); + }, + }); + } + + public start() {} + + public stop() {} +} + +export type CorePluginDeprecationsPluginSetup = ReturnType; +export type CorePluginDeprecationsPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts new file mode 100644 index 0000000000000..db4288d26a3d7 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.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 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, TypeOf } from '@kbn/config-schema'; +import { get } from 'lodash'; +import type { PluginConfigDescriptor } from 'kibana/server'; +import type { ConfigDeprecation } from '@kbn/config'; + +const configSchema = schema.object({ + newProperty: schema.maybe(schema.string({ defaultValue: 'Some string' })), + noLongerUsed: schema.maybe(schema.string()), + secret: schema.maybe(schema.number({ defaultValue: 42 })), +}); + +type ConfigType = TypeOf; + +const configSecretDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { + if (get(settings, 'corePluginDeprecations.secret') !== 42) { + addDeprecation({ + documentationUrl: 'config-secret-doc-url', + message: + 'Kibana plugin funcitonal tests will no longer allow corePluginDeprecations.secret ' + + 'config to be set to anything except 42.', + }); + } + return settings; +}; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('oldProperty', 'newProperty'), + unused('noLongerUsed'), + configSecretDeprecation, + ], +}; diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/index.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/index.ts new file mode 100644 index 0000000000000..1968c011a327a --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CorePluginDeprecationsPlugin } from './plugin'; + +export { config } from './config'; +export const plugin = () => new CorePluginDeprecationsPlugin(); diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts new file mode 100644 index 0000000000000..38565b1e2c0a8 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Plugin, CoreSetup, GetDeprecationsContext, DeprecationsDetails } from 'kibana/server'; +import { registerRoutes } from './routes'; +async function getDeprecations({ + savedObjectsClient, +}: GetDeprecationsContext): Promise { + const deprecations: DeprecationsDetails[] = []; + const { total } = await savedObjectsClient.find({ type: 'test-deprecations-plugin', perPage: 1 }); + + deprecations.push({ + message: `CorePluginDeprecationsPlugin is a deprecated feature for testing.`, + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, + }); + + if (total > 0) { + deprecations.push({ + message: `SavedObject test-deprecations-plugin is still being used.`, + documentationUrl: 'another-test-url', + level: 'critical', + correctiveActions: {}, + }); + } + + return deprecations; +} + +export class CorePluginDeprecationsPlugin implements Plugin { + public setup(core: CoreSetup, deps: {}) { + registerRoutes(core.http); + core.savedObjects.registerType({ + name: 'test-deprecations-plugin', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + }, + }, + }); + + core.deprecations.registerDeprecations({ getDeprecations }); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/routes.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/routes.ts new file mode 100644 index 0000000000000..d6bf065898f93 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/routes.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 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 { HttpServiceSetup } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export function registerRoutes(http: HttpServiceSetup) { + const router = http.createRouter(); + router.post( + { + path: '/api/core_deprecations_resolve/', + validate: { + body: schema.object({ + mockFail: schema.maybe(schema.boolean()), + keyId: schema.maybe(schema.string()), + deprecationDetails: schema.object({ + domainId: schema.string(), + }), + }), + }, + }, + async (context, req, res) => { + const { mockFail, keyId } = req.body; + if (mockFail === true) { + return res.badRequest({ + body: new Error('Mocking api failure'), + }); + } + + if (keyId) { + const client = context.core.savedObjects.getClient(); + await client.delete('test-deprecations-plugin', keyId, { + refresh: true, + }); + } + + return res.ok(); + } + ); +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json new file mode 100644 index 0000000000000..3d9d8ca9451d4 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts new file mode 100644 index 0000000000000..c44781ab284c6 --- /dev/null +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -0,0 +1,247 @@ +/* + * Copyright 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 expect from '@kbn/expect'; +import type { DomainDeprecationDetails, DeprecationsGetResponse } from 'src/core/server/types'; +import type { ResolveDeprecationResponse } from 'src/core/public'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + + const CorePluginDeprecationsPluginDeprecations = [ + { + level: 'critical', + message: + '"corePluginDeprecations.oldProperty" is deprecated and has been replaced by "corePluginDeprecations.newProperty"', + correctiveActions: { + manualSteps: [ + 'Replace "corePluginDeprecations.oldProperty" with "corePluginDeprecations.newProperty" in the Kibana config file, CLI flag, or environment variable (in Docker only).', + ], + }, + domainId: 'corePluginDeprecations', + }, + { + level: 'critical', + message: 'corePluginDeprecations.noLongerUsed is deprecated and is no longer used', + correctiveActions: { + manualSteps: [ + 'Remove "corePluginDeprecations.noLongerUsed" from the Kibana config file, CLI flag, or environment variable (in Docker only)', + ], + }, + domainId: 'corePluginDeprecations', + }, + { + level: 'critical', + message: + 'Kibana plugin funcitonal tests will no longer allow corePluginDeprecations.secret config to be set to anything except 42.', + correctiveActions: {}, + documentationUrl: 'config-secret-doc-url', + domainId: 'corePluginDeprecations', + }, + { + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, + domainId: 'corePluginDeprecations', + }, + { + message: 'SavedObject test-deprecations-plugin is still being used.', + documentationUrl: 'another-test-url', + level: 'critical', + correctiveActions: {}, + domainId: 'corePluginDeprecations', + }, + ]; + + describe('deprecations service', () => { + before(() => esArchiver.load('../functional/fixtures/es_archiver/deprecations_service')); + after(() => esArchiver.unload('../functional/fixtures/es_archiver/deprecations_service')); + + describe('GET /api/deprecations/', async () => { + it('returns registered config deprecations and feature deprecations', async () => { + const { body } = await supertest.get('/api/deprecations/').set('kbn-xsrf', 'true'); + + const { deprecations } = body as DeprecationsGetResponse; + expect(Array.isArray(deprecations)).to.be(true); + const corePluginDeprecations = deprecations.filter( + ({ domainId }) => domainId === 'corePluginDeprecations' + ); + + expect(corePluginDeprecations).to.eql(CorePluginDeprecationsPluginDeprecations); + }); + }); + + describe('Public API', () => { + before(async () => await PageObjects.common.navigateToApp('home')); + + it('#getAllDeprecations returns all deprecations plugin deprecations', async () => { + const result = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations.getAllDeprecations().then(cb); + }); + + const corePluginDeprecations = result.filter( + ({ domainId }) => domainId === 'corePluginDeprecations' + ); + + expect(corePluginDeprecations).to.eql(CorePluginDeprecationsPluginDeprecations); + }); + + it('#getDeprecations returns domain deprecations', async () => { + const corePluginDeprecations = await browser.executeAsync( + (cb) => { + return window._coreProvider.start.core.deprecations + .getDeprecations('corePluginDeprecations') + .then(cb); + } + ); + + expect(corePluginDeprecations).to.eql(CorePluginDeprecationsPluginDeprecations); + }); + + describe('resolveDeprecation', () => { + it('fails on missing correctiveActions.api', async () => { + const resolveResult = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }); + + expect(resolveResult).to.eql({ + reason: 'deprecation has no correctiveAction via api.', + status: 'fail', + }); + }); + + it('fails on bad request from correctiveActions.api', async () => { + const resolveResult = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + api: { + method: 'POST', + path: '/api/core_deprecations_resolve/', + body: { + mockFail: true, + }, + }, + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }); + + expect(resolveResult).to.eql({ + reason: 'Mocking api failure', + status: 'fail', + }); + }); + + it('fails on 404 request from correctiveActions.api', async () => { + const resolveResult = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + api: { + method: 'POST', + path: '/api/invalid_route_not_registered/', + body: { + mockFail: true, + }, + }, + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }); + + expect(resolveResult).to.eql({ + reason: 'Not Found', + status: 'fail', + }); + }); + + it('returns { status: ok } on successful correctiveActions.api', async () => { + const savedObjectId = await supertest + .get('/api/saved_objects/_find?type=test-deprecations-plugin') + .set('kbn-xsrf', 'true') + .expect(200) + .then(({ body }) => { + expect(body.total).to.be(1); + return body.saved_objects[0].id; + }); + + const resolveResult = await browser.executeAsync( + (keyId, cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + api: { + method: 'POST', + path: '/api/core_deprecations_resolve/', + body: { keyId }, + }, + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }, + savedObjectId + ); + + expect(resolveResult).to.eql({ status: 'ok' }); + await supertest + .get('/api/saved_objects/_find?type=test-deprecations-plugin') + .set('kbn-xsrf', 'true') + .expect(200) + .then(({ body }) => { + expect(body.total).to.be(0); + }); + + const { deprecations } = await supertest + .get('/api/deprecations/') + .set('kbn-xsrf', 'true') + .then( + ({ body }): Promise => { + return body; + } + ); + + const deprecation = deprecations.find( + ({ message }) => message === 'SavedObject test-deprecations-plugin is still being used.' + ); + + expect(deprecation).to.eql(undefined); + }); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core/index.ts b/test/plugin_functional/test_suites/core/index.ts index 9baa1ab0b394d..8591c2fdec8dd 100644 --- a/test/plugin_functional/test_suites/core/index.ts +++ b/test/plugin_functional/test_suites/core/index.ts @@ -10,6 +10,7 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('core', function () { + loadTestFile(require.resolve('./deprecations')); loadTestFile(require.resolve('./route')); }); } diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index fd1166b07f322..74d3c5b0cad18 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --maxWorkers=8 + node scripts/jest --ci --verbose --maxWorkers=6 diff --git a/typings/elasticsearch/aggregations.d.ts b/typings/elasticsearch/aggregations.d.ts deleted file mode 100644 index 2b501c94889f4..0000000000000 --- a/typings/elasticsearch/aggregations.d.ts +++ /dev/null @@ -1,466 +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 { Unionize, UnionToIntersection } from 'utility-types'; -import { ESSearchHit, MaybeReadonlyArray, ESSourceOptions, ESHitsOf } from '.'; - -export type SortOrder = 'asc' | 'desc'; -type SortInstruction = Record; -export type SortOptions = SortOrder | SortInstruction | SortInstruction[]; - -type Script = - | string - | { - lang?: string; - id?: string; - source?: string; - params?: Record; - }; - -type BucketsPath = string | Record; - -type AggregationSourceOptions = - | { - field: string; - missing?: unknown; - } - | { - script: Script; - }; - -interface MetricsAggregationResponsePart { - value: number | null; -} -interface DateHistogramBucket { - doc_count: number; - key: number; - key_as_string: string; -} - -type GetCompositeKeys< - TAggregationOptionsMap extends AggregationOptionsMap -> = TAggregationOptionsMap extends { - composite: { sources: Array }; -} - ? keyof Source - : never; - -type CompositeOptionsSource = Record< - string, - | { - terms: ({ field: string } | { script: Script }) & { - missing_bucket?: boolean; - }; - } - | undefined ->; - -export interface AggregationOptionsByType { - terms: { - size?: number; - order?: SortOptions; - execution_hint?: 'map' | 'global_ordinals'; - } & AggregationSourceOptions; - date_histogram: { - format?: string; - min_doc_count?: number; - extended_bounds?: { - min: number; - max: number; - }; - } & ({ calendar_interval: string } | { fixed_interval: string }) & - AggregationSourceOptions; - histogram: { - interval: number; - min_doc_count?: number; - extended_bounds?: { - min?: number | string; - max?: number | string; - }; - } & AggregationSourceOptions; - avg: AggregationSourceOptions; - max: AggregationSourceOptions; - min: AggregationSourceOptions; - sum: AggregationSourceOptions; - value_count: AggregationSourceOptions; - cardinality: AggregationSourceOptions & { - precision_threshold?: number; - }; - percentiles: { - percents?: number[]; - hdr?: { number_of_significant_value_digits: number }; - } & AggregationSourceOptions; - stats: { - field: string; - }; - extended_stats: { - field: string; - }; - string_stats: { field: string }; - top_hits: { - from?: number; - size?: number; - sort?: SortOptions; - _source?: ESSourceOptions; - fields?: MaybeReadonlyArray; - docvalue_fields?: MaybeReadonlyArray; - }; - filter: Record; - filters: { - filters: Record | any[]; - }; - sampler: { - shard_size?: number; - }; - derivative: { - buckets_path: BucketsPath; - }; - bucket_script: { - buckets_path: BucketsPath; - script?: Script; - }; - composite: { - size?: number; - sources: CompositeOptionsSource[]; - after?: Record; - }; - diversified_sampler: { - shard_size?: number; - max_docs_per_value?: number; - } & ({ script: Script } | { field: string }); // TODO use MetricsAggregationOptions if possible - scripted_metric: { - params?: Record; - init_script?: Script; - map_script: Script; - combine_script: Script; - reduce_script: Script; - }; - date_range: { - format?: string; - ranges: Array< - | { from: string | number } - | { to: string | number } - | { from: string | number; to: string | number } - >; - keyed?: boolean; - } & AggregationSourceOptions; - range: { - field: string; - ranges: Array< - | { key?: string; from: string | number } - | { key?: string; to: string | number } - | { key?: string; from: string | number; to: string | number } - >; - keyed?: boolean; - }; - auto_date_histogram: { - buckets: number; - } & AggregationSourceOptions; - percentile_ranks: { - values: Array; - keyed?: boolean; - hdr?: { number_of_significant_value_digits: number }; - } & AggregationSourceOptions; - bucket_sort: { - sort?: SortOptions; - from?: number; - size?: number; - }; - significant_terms: { - size?: number; - field?: string; - background_filter?: Record; - } & AggregationSourceOptions; - bucket_selector: { - buckets_path: { - [x: string]: string; - }; - script: string; - }; - top_metrics: { - metrics: { field: string } | MaybeReadonlyArray<{ field: string }>; - sort: SortOptions; - }; - avg_bucket: { - buckets_path: string; - gap_policy?: 'skip' | 'insert_zeros'; - format?: string; - }; - rate: { - unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; - } & ( - | { - field: string; - mode: 'sum' | 'value_count'; - } - | {} - ); -} - -type AggregationType = keyof AggregationOptionsByType; - -type AggregationOptionsMap = Unionize< - { - [TAggregationType in AggregationType]: AggregationOptionsByType[TAggregationType]; - } -> & { aggs?: AggregationInputMap }; - -interface DateRangeBucket { - key: string; - to?: number; - from?: number; - to_as_string?: string; - from_as_string?: string; - doc_count: number; -} - -export interface AggregationInputMap { - [key: string]: AggregationOptionsMap; -} - -type SubAggregationResponseOf< - TAggregationInputMap extends AggregationInputMap | undefined, - TDocument -> = TAggregationInputMap extends AggregationInputMap - ? AggregationResponseMap - : {}; - -interface AggregationResponsePart { - terms: { - buckets: Array< - { - doc_count: number; - key: string | number; - } & SubAggregationResponseOf - >; - doc_count_error_upper_bound?: number; - sum_other_doc_count?: number; - }; - histogram: { - buckets: Array< - { - doc_count: number; - key: number; - } & SubAggregationResponseOf - >; - }; - date_histogram: { - buckets: Array< - DateHistogramBucket & SubAggregationResponseOf - >; - }; - avg: MetricsAggregationResponsePart; - sum: MetricsAggregationResponsePart; - max: MetricsAggregationResponsePart; - min: MetricsAggregationResponsePart; - value_count: { value: number }; - cardinality: { - value: number; - }; - percentiles: { - values: Record; - }; - stats: { - count: number; - min: number | null; - max: number | null; - avg: number | null; - sum: number | null; - }; - extended_stats: { - count: number; - min: number | null; - max: number | null; - avg: number | null; - sum: number | null; - sum_of_squares: number | null; - variance: number | null; - std_deviation: number | null; - std_deviation_bounds: { - upper: number | null; - lower: number | null; - }; - }; - string_stats: { - count: number; - min_length: number; - max_length: number; - avg_length: number; - entropy: number; - }; - top_hits: { - hits: { - total: { - value: number; - relation: 'eq' | 'gte'; - }; - max_score: number | null; - hits: TAggregationOptionsMap extends { top_hits: AggregationOptionsByType['top_hits'] } - ? ESHitsOf - : ESSearchHit[]; - }; - }; - filter: { - doc_count: number; - } & SubAggregationResponseOf; - filters: TAggregationOptionsMap extends { filters: { filters: any[] } } - ? Array< - { doc_count: number } & AggregationResponseMap - > - : TAggregationOptionsMap extends { - filters: { - filters: Record; - }; - } - ? { - buckets: { - [key in keyof TAggregationOptionsMap['filters']['filters']]: { - doc_count: number; - } & SubAggregationResponseOf; - }; - } - : never; - sampler: { - doc_count: number; - } & SubAggregationResponseOf; - derivative: - | { - value: number; - } - | undefined; - bucket_script: - | { - value: number | null; - } - | undefined; - composite: { - after_key: { - [key in GetCompositeKeys]: TAggregationOptionsMap; - }; - buckets: Array< - { - key: Record, string | number>; - doc_count: number; - } & SubAggregationResponseOf - >; - }; - diversified_sampler: { - doc_count: number; - } & AggregationResponseMap; - scripted_metric: { - value: unknown; - }; - date_range: { - buckets: TAggregationOptionsMap extends { date_range: { keyed: true } } - ? Record - : { buckets: DateRangeBucket[] }; - }; - range: { - buckets: TAggregationOptionsMap extends { range: { keyed: true } } - ? Record< - string, - DateRangeBucket & SubAggregationResponseOf - > - : Array< - DateRangeBucket & SubAggregationResponseOf - >; - }; - auto_date_histogram: { - buckets: Array< - DateHistogramBucket & AggregationResponseMap - >; - interval: string; - }; - - percentile_ranks: { - values: TAggregationOptionsMap extends { - percentile_ranks: { keyed: false }; - } - ? Array<{ key: number; value: number }> - : Record; - }; - significant_terms: { - doc_count: number; - bg_count: number; - buckets: Array< - { - score: number; - bg_count: number; - doc_count: number; - key: string | number; - } & SubAggregationResponseOf - >; - }; - bucket_sort: undefined; - bucket_selector: undefined; - top_metrics: { - top: [ - { - sort: [string | number]; - metrics: UnionToIntersection< - TAggregationOptionsMap extends { - top_metrics: { metrics: { field: infer TFieldName } }; - } - ? TopMetricsMap - : TAggregationOptionsMap extends { - top_metrics: { metrics: MaybeReadonlyArray<{ field: infer TFieldName }> }; - } - ? TopMetricsMap - : TopMetricsMap - >; - } - ]; - }; - avg_bucket: { - value: number | null; - }; - rate: { - value: number | null; - }; -} - -type TopMetricsMap = TFieldName extends string - ? Record - : Record; - -// Type for debugging purposes. If you see an error in AggregationResponseMap -// similar to "cannot be used to index type", uncomment the type below and hover -// over it to see what aggregation response types are missing compared to the -// input map. - -// type MissingAggregationResponseTypes = Exclude< -// AggregationType, -// keyof AggregationResponsePart<{}, unknown> -// >; - -// ensures aggregations work with requests where aggregation options are a union type, -// e.g. { transaction_groups: { composite: any } | { terms: any } }. -// Union keys are not included in keyof. The type will fall back to keyof T if -// UnionToIntersection fails, which happens when there are conflicts between the union -// types, e.g. { foo: string; bar?: undefined } | { foo?: undefined; bar: string }; -export type ValidAggregationKeysOf< - T extends Record -> = keyof (UnionToIntersection extends never ? T : UnionToIntersection); - -export type AggregationResultOf< - TAggregationOptionsMap extends AggregationOptionsMap, - TDocument -> = AggregationResponsePart[AggregationType & - ValidAggregationKeysOf]; - -export type AggregationResponseMap< - TAggregationInputMap extends AggregationInputMap | undefined, - TDocument -> = TAggregationInputMap extends AggregationInputMap - ? { - [TName in keyof TAggregationInputMap]: AggregationResponsePart< - TAggregationInputMap[TName], - TDocument - >[AggregationType & ValidAggregationKeysOf]; - } - : undefined; diff --git a/typings/elasticsearch/index.d.ts b/typings/elasticsearch/index.d.ts index a84d4148f6fe7..7eaf762d353ac 100644 --- a/typings/elasticsearch/index.d.ts +++ b/typings/elasticsearch/index.d.ts @@ -5,136 +5,28 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; +import { InferSearchResponseOf, AggregateOf as AggregationResultOf, SearchHit } from './search'; -import { ValuesType } from 'utility-types'; -import { Explanation, SearchParams, SearchResponse } from 'elasticsearch'; -import { RequestParams } from '@elastic/elasticsearch'; -import { AggregationResponseMap, AggregationInputMap, SortOptions } from './aggregations'; -export { - AggregationInputMap, - AggregationOptionsByType, - AggregationResponseMap, - AggregationResultOf, - SortOptions, - ValidAggregationKeysOf, -} from './aggregations'; +export type ESFilter = estypes.QueryContainer; +export type ESSearchRequest = estypes.SearchRequest; +export type AggregationOptionsByType = Required; // Typings for Elasticsearch queries and aggregations. These are intended to be // moved to the Elasticsearch JS client at some point (see #77720.) export type MaybeReadonlyArray = T[] | readonly T[]; -interface CollapseQuery { - field: string; - inner_hits?: { - name: string; - size?: number; - sort?: SortOptions; - _source?: - | string - | string[] - | { - includes?: string | string[]; - excludes?: string | string[]; - }; - collapse?: { - field: string; - }; - }; - max_concurrent_group_searches?: number; -} - export type ESSourceOptions = boolean | string | string[]; -export type ESHitsOf< - TOptions extends - | { - size?: number; - _source?: ESSourceOptions; - docvalue_fields?: MaybeReadonlyArray; - fields?: MaybeReadonlyArray; - } - | undefined, - TDocument extends unknown -> = Array< - ESSearchHit< - TOptions extends { _source: false } ? undefined : TDocument, - TOptions extends { fields: MaybeReadonlyArray } ? TOptions['fields'] : undefined, - TOptions extends { docvalue_fields: MaybeReadonlyArray } - ? TOptions['docvalue_fields'] - : undefined - > ->; - -export interface ESSearchBody { - query?: any; - size?: number; - from?: number; - aggs?: AggregationInputMap; - track_total_hits?: boolean | number; - collapse?: CollapseQuery; - search_after?: Array; - _source?: ESSourceOptions; -} - -export type ESSearchRequest = RequestParams.Search; - export interface ESSearchOptions { restTotalHitsAsInt: boolean; } -export type ESSearchHit< - TSource extends any = unknown, - TFields extends MaybeReadonlyArray | undefined = undefined, - TDocValueFields extends MaybeReadonlyArray | undefined = undefined -> = { - _index: string; - _type: string; - _id: string; - _score: number; - _version?: number; - _explanation?: Explanation; - highlight?: any; - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; -} & (TSource extends false ? {} : { _source: TSource }) & - (TFields extends MaybeReadonlyArray - ? { - fields: Partial, unknown[]>>; - } - : {}) & - (TDocValueFields extends MaybeReadonlyArray - ? { - fields: Partial, unknown[]>>; - } - : {}); - export type ESSearchResponse< - TDocument, - TSearchRequest extends ESSearchRequest, - TOptions extends ESSearchOptions = { restTotalHitsAsInt: false } -> = Omit, 'aggregations' | 'hits'> & - (TSearchRequest extends { body: { aggs: AggregationInputMap } } - ? { - aggregations?: AggregationResponseMap; - } - : {}) & { - hits: Omit['hits'], 'total' | 'hits'> & - (TOptions['restTotalHitsAsInt'] extends true - ? { - total: number; - } - : { - total: { - value: number; - relation: 'eq' | 'gte'; - }; - }) & { hits: ESHitsOf }; - }; + TDocument = unknown, + TSearchRequest extends ESSearchRequest = ESSearchRequest, + TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false } +> = InferSearchResponseOf; -export interface ESFilter { - [key: string]: { - [key: string]: string | string[] | number | boolean | Record | ESFilter[]; - }; -} +export { InferSearchResponseOf, AggregationResultOf, SearchHit }; diff --git a/typings/elasticsearch/search.d.ts b/typings/elasticsearch/search.d.ts new file mode 100644 index 0000000000000..fce08df1c0a04 --- /dev/null +++ b/typings/elasticsearch/search.d.ts @@ -0,0 +1,577 @@ +/* + * Copyright 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 { ValuesType } from 'utility-types'; +import { estypes } from '@elastic/elasticsearch'; + +type InvalidAggregationRequest = unknown; + +// ensures aggregations work with requests where aggregation options are a union type, +// e.g. { transaction_groups: { composite: any } | { terms: any } }. +// Union keys are not included in keyof, but extends iterates over the types in a union. +type ValidAggregationKeysOf> = T extends T ? keyof T : never; + +type KeyOfSource = Record< + keyof T, + (T extends Record ? null : never) | string | number +>; + +type KeysOfSources = T extends [infer U, ...infer V] + ? KeyOfSource & KeysOfSources + : T extends Array + ? KeyOfSource + : {}; + +type CompositeKeysOf< + TAggregationContainer extends estypes.AggregationContainer +> = TAggregationContainer extends { + composite: { sources: [...infer TSource] }; +} + ? KeysOfSources + : unknown; + +type Source = estypes.SourceFilter | boolean | estypes.Fields; + +type ValueTypeOfField = T extends Record + ? ValuesType + : T extends string[] | number[] + ? ValueTypeOfField> + : T extends { field: estypes.Field } + ? T['field'] + : T extends string | number + ? T + : never; + +type MaybeArray = T | T[]; + +type Fields = MaybeArray; +type DocValueFields = MaybeArray; + +export type SearchHit< + TSource extends any = unknown, + TFields extends Fields | undefined = undefined, + TDocValueFields extends DocValueFields | undefined = undefined +> = Omit & + (TSource extends false ? {} : { _source: TSource }) & + (TFields extends estypes.Fields + ? { + fields: Partial, unknown[]>>; + } + : {}) & + (TDocValueFields extends DocValueFields + ? { + fields: Partial, unknown[]>>; + } + : {}); + +type HitsOf< + TOptions extends + | { _source?: Source; fields?: Fields; docvalue_fields?: DocValueFields } + | undefined, + TDocument extends unknown +> = Array< + SearchHit< + TOptions extends { _source: false } ? undefined : TDocument, + TOptions extends { fields: estypes.Fields } ? TOptions['fields'] : undefined, + TOptions extends { docvalue_fields: DocValueFields } ? TOptions['docvalue_fields'] : undefined + > +>; + +type AggregationTypeName = Exclude; + +type AggregationMap = Partial>; + +type TopLevelAggregationRequest = Pick; + +type MaybeKeyed< + TAggregationContainer, + TBucket, + TKeys extends string = string +> = TAggregationContainer extends Record + ? Record + : { buckets: TBucket[] }; + +export type AggregateOf< + TAggregationContainer extends estypes.AggregationContainer, + TDocument +> = (Record & { + adjacency_matrix: { + buckets: Array< + { + key: string; + doc_count: number; + } & SubAggregateOf + >; + }; + auto_date_histogram: { + interval: string; + buckets: Array< + { + key: number; + key_as_string: string; + doc_count: number; + } & SubAggregateOf + >; + }; + avg: { + value: number | null; + value_as_string?: string; + }; + avg_bucket: { + value: number | null; + }; + boxplot: { + min: number | null; + max: number | null; + q1: number | null; + q2: number | null; + q3: number | null; + }; + bucket_script: { + value: unknown; + }; + cardinality: { + value: number; + }; + children: { + doc_count: number; + } & SubAggregateOf; + composite: { + after_key: CompositeKeysOf; + buckets: Array< + { + doc_count: number; + key: CompositeKeysOf; + } & SubAggregateOf + >; + }; + cumulative_cardinality: { + value: number; + }; + cumulative_sum: { + value: number; + }; + date_histogram: MaybeKeyed< + TAggregationContainer, + { + key: number; + key_as_string: string; + doc_count: number; + } & SubAggregateOf + >; + date_range: MaybeKeyed< + TAggregationContainer, + Partial<{ from: string | number; from_as_string: string }> & + Partial<{ to: string | number; to_as_string: string }> & { + doc_count: number; + key: string; + } + >; + derivative: + | { + value: number | null; + } + | undefined; + extended_stats: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number; + sum_of_squares: number | null; + variance: number | null; + variance_population: number | null; + variance_sampling: number | null; + std_deviation: number | null; + std_deviation_population: number | null; + std_deviation_sampling: number | null; + std_deviation_bounds: { + upper: number | null; + lower: number | null; + upper_population: number | null; + lower_population: number | null; + upper_sampling: number | null; + lower_sampling: number | null; + }; + } & ( + | { + min_as_string: string; + max_as_string: string; + avg_as_string: string; + sum_of_squares_as_string: string; + variance_population_as_string: string; + variance_sampling_as_string: string; + std_deviation_as_string: string; + std_deviation_population_as_string: string; + std_deviation_sampling_as_string: string; + std_deviation_bounds_as_string: { + upper: string; + lower: string; + upper_population: string; + lower_population: string; + upper_sampling: string; + lower_sampling: string; + }; + } + | {} + ); + extended_stats_bucket: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number | null; + sum_of_squares: number | null; + variance: number | null; + variance_population: number | null; + variance_sampling: number | null; + std_deviation: number | null; + std_deviation_population: number | null; + std_deviation_sampling: number | null; + std_deviation_bounds: { + upper: number | null; + lower: number | null; + upper_population: number | null; + lower_population: number | null; + upper_sampling: number | null; + lower_sampling: number | null; + }; + }; + filter: { + doc_count: number; + } & SubAggregateOf; + filters: { + buckets: TAggregationContainer extends { filters: { filters: any[] } } + ? Array< + { + doc_count: number; + } & SubAggregateOf + > + : TAggregationContainer extends { filters: { filters: Record } } + ? { + [key in keyof TAggregationContainer['filters']['filters']]: { + doc_count: number; + } & SubAggregateOf; + } & + (TAggregationContainer extends { filters: { other_bucket_key: infer TOtherBucketKey } } + ? Record< + TOtherBucketKey & string, + { doc_count: number } & SubAggregateOf + > + : unknown) & + (TAggregationContainer extends { filters: { other_bucket: true } } + ? { _other: { doc_count: number } & SubAggregateOf } + : unknown) + : unknown; + }; + geo_bounds: { + top_left: { + lat: number | null; + lon: number | null; + }; + bottom_right: { + lat: number | null; + lon: number | null; + }; + }; + geo_centroid: { + count: number; + location: { + lat: number; + lon: number; + }; + }; + geo_distance: MaybeKeyed< + TAggregationContainer, + { + from: number; + to?: number; + doc_count: number; + } & SubAggregateOf + >; + geo_hash: { + buckets: Array< + { + doc_count: number; + key: string; + } & SubAggregateOf + >; + }; + geotile_grid: { + buckets: Array< + { + doc_count: number; + key: string; + } & SubAggregateOf + >; + }; + global: { + doc_count: number; + } & SubAggregateOf; + histogram: MaybeKeyed< + TAggregationContainer, + { + key: number; + doc_count: number; + } & SubAggregateOf + >; + ip_range: MaybeKeyed< + TAggregationContainer, + { + key: string; + from?: string; + to?: string; + doc_count: number; + }, + TAggregationContainer extends { ip_range: { ranges: Array } } + ? TRangeType extends { key: infer TKeys } + ? TKeys + : string + : string + >; + inference: { + value: number; + prediction_probability: number; + prediction_score: number; + }; + max: { + value: number | null; + value_as_string?: string; + }; + max_bucket: { + value: number | null; + }; + min: { + value: number | null; + value_as_string?: string; + }; + min_bucket: { + value: number | null; + }; + median_absolute_deviation: { + value: number | null; + }; + moving_avg: + | { + value: number | null; + } + | undefined; + moving_fn: { + value: number | null; + }; + moving_percentiles: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record | undefined; + missing: { + doc_count: number; + } & SubAggregateOf; + nested: { + doc_count: number; + } & SubAggregateOf; + normalize: { + value: number | null; + // TODO: should be perhaps based on input? ie when `format` is specified + value_as_string?: string; + }; + parent: { + doc_count: number; + } & SubAggregateOf; + percentiles: { + values: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record; + }; + percentile_ranks: { + values: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record; + }; + percentiles_bucket: { + values: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record; + }; + range: MaybeKeyed< + TAggregationContainer, + { + key: string; + from?: number; + to?: number; + doc_count: number; + }, + TAggregationContainer extends { range: { ranges: Array } } + ? TRangeType extends { key: infer TKeys } + ? TKeys + : string + : string + >; + rare_terms: Array< + { + key: string | number; + doc_count: number; + } & SubAggregateOf + >; + rate: { + value: number | null; + }; + reverse_nested: { + doc_count: number; + } & SubAggregateOf; + sampler: { + doc_count: number; + } & SubAggregateOf; + scripted_metric: { + value: unknown; + }; + serial_diff: { + value: number | null; + // TODO: should be perhaps based on input? ie when `format` is specified + value_as_string?: string; + }; + significant_terms: { + doc_count: number; + bg_count: number; + buckets: Array< + { + key: string | number; + score: number; + doc_count: number; + bg_count: number; + } & SubAggregateOf + >; + }; + significant_text: { + doc_count: number; + buckets: Array<{ + key: string; + doc_count: number; + score: number; + bg_count: number; + }>; + }; + stats: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number; + } & ( + | { + min_as_string: string; + max_as_string: string; + avg_as_string: string; + sum_as_string: string; + } + | {} + ); + stats_bucket: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number; + }; + string_stats: { + count: number; + min_length: number | null; + max_length: number | null; + avg_length: number | null; + entropy: number | null; + distribution: Record; + }; + sum: { + value: number | null; + value_as_string?: string; + }; + sum_bucket: { + value: number | null; + }; + terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: Array< + { + doc_count: number; + key: string | number; + } & SubAggregateOf + >; + }; + top_hits: { + hits: { + total: { + value: number; + relation: 'eq' | 'gte'; + }; + max_score: number | null; + hits: TAggregationContainer extends { top_hits: estypes.TopHitsAggregation } + ? HitsOf + : estypes.HitsMetadata; + }; + }; + top_metrics: { + top: Array<{ + sort: number[] | string[]; + metrics: Record< + TAggregationContainer extends Record }> + ? TKeys + : string, + string | number | null + >; + }>; + }; + weighted_avg: { value: number | null }; + value_count: { + value: number; + }; + // t_test: {} not defined +})[ValidAggregationKeysOf & AggregationTypeName]; + +type AggregateOfMap = { + [TAggregationName in keyof TAggregationMap]: TAggregationMap[TAggregationName] extends estypes.AggregationContainer + ? AggregateOf + : never; // using never means we effectively ignore optional keys, using {} creates a union type of { ... } | {} +}; + +type SubAggregateOf = TAggregationRequest extends { + aggs?: AggregationMap; +} + ? AggregateOfMap + : TAggregationRequest extends { aggregations?: AggregationMap } + ? AggregateOfMap + : {}; + +type SearchResponseOf< + TAggregationRequest extends TopLevelAggregationRequest, + TDocument +> = SubAggregateOf; + +// if aggregation response cannot be inferred, fall back to unknown +type WrapAggregationResponse = keyof T extends never + ? { aggregations?: unknown } + : { aggregations?: T }; + +export type InferSearchResponseOf< + TDocument = unknown, + TSearchRequest extends estypes.SearchRequest = estypes.SearchRequest, + TOptions extends { restTotalHitsAsInt?: boolean } = {} +> = Omit, 'aggregations' | 'hits'> & + (TSearchRequest['body'] extends TopLevelAggregationRequest + ? WrapAggregationResponse> + : { aggregations?: InvalidAggregationRequest }) & { + hits: Omit['hits'], 'total' | 'hits'> & + (TOptions['restTotalHitsAsInt'] extends true + ? { + total: number; + } + : { + total: { + value: number; + relation: 'eq' | 'gte'; + }; + }) & { hits: HitsOf }; + }; diff --git a/x-pack/examples/alerting_example/public/components/view_alert.tsx b/x-pack/examples/alerting_example/public/components/view_alert.tsx index 8c942d685af27..40eeb9fd360dc 100644 --- a/x-pack/examples/alerting_example/public/components/view_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_alert.tsx @@ -21,8 +21,12 @@ import { import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; -import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerting/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { + Alert, + AlertTaskState, + LEGACY_BASE_ALERT_API_PATH, +} from '../../../../plugins/alerting/common'; type Props = RouteComponentProps & { http: CoreStart['http']; @@ -34,10 +38,10 @@ export const ViewAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); + http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); + http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); diff --git a/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx index 7e8487b0179fa..8eef1882b9389 100644 --- a/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx @@ -23,8 +23,12 @@ import { import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; -import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerting/common'; import { ALERTING_EXAMPLE_APP_ID, AlwaysFiringParams } from '../../common/constants'; +import { + Alert, + AlertTaskState, + LEGACY_BASE_ALERT_API_PATH, +} from '../../../../plugins/alerting/common'; type Props = RouteComponentProps & { http: CoreStart['http']; @@ -40,10 +44,10 @@ export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); + http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); + http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index 71af1b824eb80..d2af98e8e85e4 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -111,7 +111,13 @@ export const App = (props: { defaultIndexPattern: IndexPattern | null; }) => { const [color, setColor] = useState('green'); + const [isLoading, setIsLoading] = useState(false); const LensComponent = props.plugins.lens.EmbeddableComponent; + + const [time, setTime] = useState({ + from: 'now-5d', + to: 'now', + }); return ( @@ -138,6 +144,7 @@ export const App = (props: { { // eslint-disable-next-line no-bitwise const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); @@ -153,10 +160,7 @@ export const App = (props: { onClick={() => { props.plugins.lens.navigateToPrefilledEditor({ id: '', - timeRange: { - from: 'now-5d', - to: 'now', - }, + timeRange: time, attributes: getLensAttributes(props.defaultIndexPattern!, color), }); // eslint-disable-next-line no-bitwise @@ -171,11 +175,23 @@ export const App = (props: { { + setIsLoading(val); + }} + onBrushEnd={({ range }) => { + setTime({ + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }); + }} + onFilter={(_data) => { + // call back event for on filter event + }} + onTableRowClick={(_data) => { + // call back event for on table row click event + }} /> ) : ( diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 98b9b46fac48d..a333d86b27129 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -744,6 +744,7 @@ describe('getAll()', () => { }; unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult); scopedClusterClient.asInternalUser.search.mockResolvedValueOnce( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { '1': { doc_count: 6 }, @@ -817,6 +818,7 @@ describe('getAll()', () => { ], }); scopedClusterClient.asInternalUser.search.mockResolvedValueOnce( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { '1': { doc_count: 6 }, @@ -877,6 +879,7 @@ describe('getAll()', () => { }; unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult); scopedClusterClient.asInternalUser.search.mockResolvedValueOnce( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { '1': { doc_count: 6 }, @@ -949,6 +952,7 @@ describe('getBulk()', () => { ], }); scopedClusterClient.asInternalUser.search.mockResolvedValueOnce( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { '1': { doc_count: 6 }, @@ -1019,6 +1023,7 @@ describe('getBulk()', () => { ], }); scopedClusterClient.asInternalUser.search.mockResolvedValueOnce( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { '1': { doc_count: 6 }, @@ -1076,6 +1081,7 @@ describe('getBulk()', () => { ], }); scopedClusterClient.asInternalUser.search.mockResolvedValueOnce( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { '1': { doc_count: 6 }, diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 2e2b3e7a6d814..d8dcde2fab103 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import type { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; import { omitBy, isUndefined } from 'lodash'; @@ -509,7 +510,7 @@ async function injectExtraFindData( scopedClusterClient: IScopedClusterClient, actionResults: ActionResult[] ): Promise { - const aggs: Record = {}; + const aggs: Record = {}; for (const actionResult of actionResults) { aggs[actionResult.id] = { filter: { @@ -555,6 +556,7 @@ async function injectExtraFindData( }); return actionResults.map((actionResult) => ({ ...actionResult, + // @ts-expect-error aggegation type is not specified referencedByCount: aggregationResult.aggregations[actionResult.id].doc_count, })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 67ba7ffea10e8..f7b0e7de478d8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -93,8 +93,8 @@ async function executor( const err = find(result.items, 'index.error.reason'); if (err) { return wrapErr( - `${err.index.error!.reason}${ - err.index.error?.caused_by ? ` (${err.index.error?.caused_by?.reason})` : '' + `${err.index?.error?.reason}${ + err.index?.error?.caused_by ? ` (${err.index?.error?.caused_by?.reason})` : '' }`, actionId, logger diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts index a998fc7af0c99..e4611857ca279 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts @@ -13,6 +13,7 @@ describe('actions telemetry', () => { test('getTotalCount should replace first symbol . to __ for action types names', async () => { const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; mockEsClient.search.mockReturnValue( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { byActionTypeId: { @@ -116,6 +117,7 @@ Object { test('getInUseTotalCount', async () => { const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; mockEsClient.search.mockReturnValue( + // @ts-expect-error not full search response elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { refs: { diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index 6973a7e8dcbd2..8d028b176a00a 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -53,21 +53,19 @@ export async function getTotalCount(esClient: ElasticsearchClient, kibanaIndex: }, }, }); - + // @ts-expect-error aggegation type is not specified + const aggs = searchResult.aggregations?.byActionTypeId.value?.types; return { - countTotal: Object.keys(searchResult.aggregations.byActionTypeId.value.types).reduce( - (total: number, key: string) => - parseInt(searchResult.aggregations.byActionTypeId.value.types[key], 0) + total, + countTotal: Object.keys(aggs).reduce( + (total: number, key: string) => parseInt(aggs[key], 0) + total, 0 ), - countByType: Object.keys(searchResult.aggregations.byActionTypeId.value.types).reduce( + countByType: Object.keys(aggs).reduce( // ES DSL aggregations are returned as `any` by esClient.search // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, - [replaceFirstAndLastDotSymbols(key)]: searchResult.aggregations.byActionTypeId.value.types[ - key - ], + [replaceFirstAndLastDotSymbols(key)]: aggs[key], }), {} ), @@ -161,9 +159,9 @@ export async function getInUseTotalCount( }, }); - const bulkFilter = Object.entries( - actionResults.aggregations.refs.actionRefIds.value.connectorIds - ).map(([key]) => ({ + // @ts-expect-error aggegation type is not specified + const aggs = actionResults.aggregations.refs.actionRefIds.value; + const bulkFilter = Object.entries(aggs.connectorIds).map(([key]) => ({ id: key, type: 'action', fields: ['id', 'actionTypeId'], @@ -179,7 +177,7 @@ export async function getInUseTotalCount( }, {} ); - return { countTotal: actionResults.aggregations.refs.actionRefIds.value.total, countByType }; + return { countTotal: aggs.total, countByType }; } function replaceFirstAndLastDotSymbols(strToReplace: string) { diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts index f8a91e3a0a67a..c338bbc998c49 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts @@ -23,6 +23,7 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { __servicenow: { type: 'long' }, __jira: { type: 'long' }, __resilient: { type: 'long' }, + __teams: { type: 'long' }, }; export function createActionsUsageCollector( diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 19322fed7363e..eb64d71be565e 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -453,20 +453,20 @@ The only case in which this handler will not be used to evaluate the navigation You can use the `registerNavigation` api to specify as many AlertType specific handlers as you like, but you can only use it once per AlertType as we wouldn't know which handler to use if you specified two for the same AlertType. For the same reason, you can only use `registerDefaultNavigation` once per plugin, as it covers all cases for your specific plugin. -## Experimental RESTful API +## Internal HTTP APIs -Using of the alert type requires you to create an alert that will contain parameters and actions for a given alert type. API description for CRUD operations is a part of the [user documentation](https://www.elastic.co/guide/en/kibana/master/alerts-api-update.html). -API listed below is experimental and could be changed or removed in the future. +Using of the rule type requires you to create a rule that will contain parameters and actions for a given rule type. API description for CRUD operations is a part of the [user documentation](https://www.elastic.co/guide/en/kibana/master/alerting-apis.html). +API listed below are internal and should not be consumed by plugin outside the alerting plugins. -### `GET /api/alerts/alert/{id}/state`: Get alert state +### `GET /internal/alerting/rule/{id}/state`: Get rule state Params: |Property|Description|Type| |---|---|---| -|id|The id of the alert whose state you're trying to get.|string| +|id|The id of the rule whose state you're trying to get.|string| -### `GET /api/alerts/alert/{id}/_instance_summary`: Get alert instance summary +### `GET /internal/alerting/rule/{id}/_alert_summary`: Get rule alert summary Similar to the `GET state` call, but collects additional information from the event log. @@ -475,7 +475,7 @@ Params: |Property|Description|Type| |---|---|---| -|id|The id of the alert whose instance summary you're trying to get.|string| +|id|The id of the rule whose alert summary you're trying to get.|string| Query: @@ -483,11 +483,11 @@ Query: |---|---|---| |dateStart|The date to start looking for alert events in the event log. Either an ISO date string, or a duration string indicating time since now.|string| -### `POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key +### `POST /internal/alerting/rule/{id}/_update_api_key`: Update rule API key |Property|Description|Type| |---|---|---| -|id|The id of the alert you're trying to update the API key for. System will use user in request context to generate an API key for.|string| +|id|The id of the rule you're trying to update the API key for. System will use user in request context to generate an API key for.|string| ## Alert instance factory diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index ff1540090a357..3530abb7384ea 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -24,5 +24,7 @@ export interface AlertingFrameworkHealth { alertingFrameworkHeath: AlertsHealth; } -export const BASE_ALERT_API_PATH = '/api/alerts'; +export const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; +export const BASE_ALERTING_API_PATH = '/api/alerting'; +export const INTERNAL_BASE_ALERTING_API_PATH = '/internal/alerting'; export const ALERTS_FEATURE_ID = 'alerts'; diff --git a/x-pack/plugins/alerting/public/alert_api.ts b/x-pack/plugins/alerting/public/alert_api.ts index 6eb2e29a7e8e5..d1213c80b95be 100644 --- a/x-pack/plugins/alerting/public/alert_api.ts +++ b/x-pack/plugins/alerting/public/alert_api.ts @@ -7,11 +7,11 @@ import { HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { BASE_ALERT_API_PATH } from '../common'; +import { LEGACY_BASE_ALERT_API_PATH } from '../common'; import type { Alert, AlertType } from '../common'; export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`); + return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/list_alert_types`); } export async function loadAlertType({ @@ -22,7 +22,7 @@ export async function loadAlertType({ id: AlertType['id']; }): Promise { const maybeAlertType = ((await http.get( - `${BASE_ALERT_API_PATH}/list_alert_types` + `${LEGACY_BASE_ALERT_API_PATH}/list_alert_types` )) as AlertType[]).find((type) => type.id === id); if (!maybeAlertType) { throw new Error( @@ -44,5 +44,5 @@ export async function loadAlert({ http: HttpSetup; alertId: string; }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/alert/${alertId}`); + return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}`); } diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 1b1075f4d7cf1..e316ecd3c6fec 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -8,6 +8,7 @@ import Boom from '@hapi/boom'; import { omit, isEqual, map, uniq, pick, truncate, trim } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { estypes } from '@elastic/elasticsearch'; import { Logger, SavedObjectsClientContract, @@ -100,7 +101,7 @@ export interface FindOptions extends IndexType { defaultSearchOperator?: 'AND' | 'OR'; searchFields?: string[]; sortField?: string; - sortOrder?: string; + sortOrder?: estypes.SortOrder; hasReference?: { type: string; id: string; @@ -124,7 +125,7 @@ interface IndexType { [key: string]: unknown; } -interface AggregateResult { +export interface AggregateResult { alertExecutionStatus: { [status: string]: number }; } @@ -156,7 +157,7 @@ export interface CreateOptions { }; } -interface UpdateOptions { +export interface UpdateOptions { id: string; data: { name: string; @@ -169,7 +170,7 @@ interface UpdateOptions { }; } -interface GetAlertInstanceSummaryParams { +export interface GetAlertInstanceSummaryParams { id: string; dateStart?: string; } @@ -228,7 +229,7 @@ export class AlertsClient { public async create({ data, options, - }: CreateOptions): Promise> { + }: CreateOptions): Promise> { const id = options?.id || SavedObjectsUtils.generateId(); try { diff --git a/x-pack/plugins/alerting/server/lib/errors/index.ts b/x-pack/plugins/alerting/server/lib/errors/index.ts new file mode 100644 index 0000000000000..9c6d233f15d3d --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/errors/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export function isErrorThatHandlesItsOwnResponse( + e: ErrorThatHandlesItsOwnResponse +): e is ErrorThatHandlesItsOwnResponse { + return typeof (e as ErrorThatHandlesItsOwnResponse).sendResponse === 'function'; +} + +export { ErrorThatHandlesItsOwnResponse }; +export { AlertTypeDisabledError, AlertTypeDisabledReason } from './alert_type_disabled'; diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index 3fd0ac403f8f8..493b004106933 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -6,10 +6,17 @@ */ export { parseDuration, validateDurationSchema } from '../../common/parse_duration'; -export { LicenseState } from './license_state'; +export { ILicenseState, LicenseState } from './license_state'; export { validateAlertTypeParams } from './validate_alert_type_params'; export { getAlertNotifyWhenType } from './get_alert_notify_when_type'; +export { verifyApiAccess } from './license_api_access'; export { ErrorWithReason, getReasonFromError, isErrorWithReason } from './error_with_reason'; +export { + AlertTypeDisabledError, + AlertTypeDisabledReason, + ErrorThatHandlesItsOwnResponse, + isErrorThatHandlesItsOwnResponse, +} from './errors'; export { executionStatusFromState, executionStatusFromError, diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index ff36ebcd84ba5..787d3cc548ba1 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -37,25 +37,7 @@ import { } from '../../../../src/core/server'; import type { AlertingRequestHandlerContext } from './types'; -import { - aggregateAlertRoute, - createAlertRoute, - deleteAlertRoute, - findAlertRoute, - getAlertRoute, - getAlertStateRoute, - getAlertInstanceSummaryRoute, - listAlertTypesRoute, - updateAlertRoute, - enableAlertRoute, - disableAlertRoute, - updateApiKeyRoute, - muteAllAlertRoute, - unmuteAllAlertRoute, - muteAlertInstanceRoute, - unmuteAlertInstanceRoute, - healthRoute, -} from './routes'; +import { defineRoutes } from './routes'; import { LICENSE_TYPE, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as ActionsPluginSetupContract, @@ -266,23 +248,7 @@ export class AlertingPlugin { // Routes const router = core.http.createRouter(); // Register routes - aggregateAlertRoute(router, this.licenseState); - createAlertRoute(router, this.licenseState); - deleteAlertRoute(router, this.licenseState); - findAlertRoute(router, this.licenseState); - getAlertRoute(router, this.licenseState); - getAlertStateRoute(router, this.licenseState); - getAlertInstanceSummaryRoute(router, this.licenseState); - listAlertTypesRoute(router, this.licenseState); - updateAlertRoute(router, this.licenseState); - enableAlertRoute(router, this.licenseState); - disableAlertRoute(router, this.licenseState); - updateApiKeyRoute(router, this.licenseState); - muteAllAlertRoute(router, this.licenseState); - unmuteAllAlertRoute(router, this.licenseState); - muteAlertInstanceRoute(router, this.licenseState); - unmuteAlertInstanceRoute(router, this.licenseState); - healthRoute(router, this.licenseState, plugins.encryptedSavedObjects); + defineRoutes(router, this.licenseState, plugins.encryptedSavedObjects); return { registerType< diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts new file mode 100644 index 0000000000000..26c06eae33d0a --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { aggregateRulesRoute } from './aggregate_rules'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('aggregateRulesRoute', () => { + it('aggregate rules with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + aggregateRulesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rules/_aggregate"`); + + const aggregateResult = { + alertExecutionStatus: { + ok: 15, + error: 2, + active: 23, + pending: 1, + unknown: 0, + }, + }; + alertsClient.aggregate.mockResolvedValueOnce(aggregateResult); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + default_search_operator: 'AND', + }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "rule_execution_status": Object { + "active": 23, + "error": 2, + "ok": 15, + "pending": 1, + "unknown": 0, + }, + }, + } + `); + + expect(alertsClient.aggregate).toHaveBeenCalledTimes(1); + expect(alertsClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "options": Object { + "defaultSearchOperator": "AND", + }, + }, + ] + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + rule_execution_status: { + ok: 15, + error: 2, + active: 23, + pending: 1, + unknown: 0, + }, + }, + }); + }); + + it('ensures the license allows aggregating rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + aggregateRulesRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.aggregate.mockResolvedValueOnce({ + alertExecutionStatus: { + ok: 15, + error: 2, + active: 23, + pending: 1, + unknown: 0, + }, + }); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + default_search_operator: 'OR', + }, + } + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents aggregating rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + aggregateRulesRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + query: {}, + }, + ['ok'] + ); + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts new file mode 100644 index 0000000000000..ecebd7851af6b --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState } from '../lib'; +import { AggregateResult, AggregateOptions } from '../alerts_client'; +import { RewriteResponseCase, RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; + +// config definition +const querySchema = schema.object({ + search: schema.maybe(schema.string()), + default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { + defaultValue: 'OR', + }), + search_fields: schema.maybe(schema.arrayOf(schema.string())), + has_reference: schema.maybe( + // use nullable as maybe is currently broken + // in config-schema + schema.nullable( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ) + ), + filter: schema.maybe(schema.string()), +}); + +const rewriteQueryReq: RewriteRequestCase = ({ + default_search_operator: defaultSearchOperator, + has_reference: hasReference, + search_fields: searchFields, + ...rest +}) => ({ + ...rest, + defaultSearchOperator, + ...(hasReference ? { hasReference } : {}), + ...(searchFields ? { searchFields } : {}), +}); +const rewriteBodyRes: RewriteResponseCase = ({ + alertExecutionStatus, + ...rest +}) => ({ + ...rest, + rule_execution_status: alertExecutionStatus, +}); + +export const aggregateRulesRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, + validate: { + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const options = rewriteQueryReq({ + ...req.query, + has_reference: req.query.has_reference || undefined, + }); + const aggregateResult = await alertsClient.aggregate({ options }); + return res.ok({ + body: rewriteBodyRes(aggregateResult), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/create_rule.test.ts b/x-pack/plugins/alerting/server/routes/create_rule.test.ts new file mode 100644 index 0000000000000..5dbc5014ef6ba --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/create_rule.test.ts @@ -0,0 +1,298 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { pick } from 'lodash'; +import { createRuleRoute } from './create_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { CreateOptions } from '../alerts_client'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib'; +import { AsApiContract } from './lib'; +import { SanitizedAlert } from '../types'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('createRuleRoute', () => { + const createdAt = new Date(); + const updatedAt = new Date(); + + const mockedAlert: SanitizedAlert<{ bar: boolean }> = { + alertTypeId: '1', + consumer: 'bar', + name: 'abc', + schedule: { interval: '10s' }, + tags: ['foo'], + params: { + bar: true, + }, + throttle: '30s', + actions: [ + { + actionTypeId: 'test', + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + enabled: true, + muteAll: false, + createdBy: '', + updatedBy: '', + apiKeyOwner: '', + mutedInstanceIds: [], + notifyWhen: 'onActionGroupChange', + createdAt, + updatedAt, + id: '123', + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + }; + + const ruleToCreate: AsApiContract['data']> = { + ...pick(mockedAlert, 'consumer', 'name', 'schedule', 'tags', 'params', 'throttle', 'enabled'), + rule_type_id: mockedAlert.alertTypeId, + notify_when: mockedAlert.notifyWhen, + actions: [ + { + group: mockedAlert.actions[0].group, + id: mockedAlert.actions[0].id, + params: mockedAlert.actions[0].params, + }, + ], + }; + + const createResult: AsApiContract> = { + ...ruleToCreate, + mute_all: mockedAlert.muteAll, + created_by: mockedAlert.createdBy, + updated_by: mockedAlert.updatedBy, + api_key_owner: mockedAlert.apiKeyOwner, + muted_alert_ids: mockedAlert.mutedInstanceIds, + created_at: mockedAlert.createdAt, + updated_at: mockedAlert.updatedAt, + id: mockedAlert.id, + execution_status: { + status: mockedAlert.executionStatus.status, + last_execution_date: mockedAlert.executionStatus.lastExecutionDate, + }, + actions: [ + { + ...ruleToCreate.actions[0], + connector_type_id: 'test', + }, + ], + }; + + it('creates a rule with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id?}"`); + + alertsClient.create.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + body: ruleToCreate, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toEqual({ body: createResult }); + + expect(alertsClient.create).toHaveBeenCalledTimes(1); + expect(alertsClient.create.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "actions": Array [ + Object { + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "1", + "consumer": "bar", + "enabled": true, + "name": "abc", + "notifyWhen": "onActionGroupChange", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "tags": Array [ + "foo", + ], + "throttle": "30s", + }, + "options": Object { + "id": undefined, + }, + }, + ] + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: createResult, + }); + }); + + it('allows providing a custom id', async () => { + const expectedResult = { + ...createResult, + id: 'custom-id', + }; + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id?}"`); + + alertsClient.create.mockResolvedValueOnce({ + ...mockedAlert, + id: 'custom-id', + }); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: 'custom-id' }, + body: ruleToCreate, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toEqual({ body: expectedResult }); + + expect(alertsClient.create).toHaveBeenCalledTimes(1); + expect(alertsClient.create.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "actions": Array [ + Object { + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "1", + "consumer": "bar", + "enabled": true, + "name": "abc", + "notifyWhen": "onActionGroupChange", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "tags": Array [ + "foo", + ], + "throttle": "30s", + }, + "options": Object { + "id": "custom-id", + }, + }, + ] + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: expectedResult, + }); + }); + + it('ensures the license allows creating rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.create.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { body: ruleToCreate }); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents creating rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + createRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.create.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.create.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { body: ruleToCreate }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/create_rule.ts b/x-pack/plugins/alerting/server/routes/create_rule.ts new file mode 100644 index 0000000000000..4e31db970ccc6 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/create_rule.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { validateDurationSchema, ILicenseState, AlertTypeDisabledError } from '../lib'; +import { CreateOptions } from '../alerts_client'; +import { + RewriteRequestCase, + RewriteResponseCase, + handleDisabledApiKeysError, + verifyAccessAndContext, +} from './lib'; +import { + SanitizedAlert, + validateNotifyWhenType, + AlertTypeParams, + AlertingRequestHandlerContext, + BASE_ALERTING_API_PATH, + AlertNotifyWhenType, +} from '../types'; + +export const bodySchema = schema.object({ + name: schema.string(), + rule_type_id: schema.string(), + enabled: schema.boolean({ defaultValue: true }), + consumer: schema.string(), + tags: schema.arrayOf(schema.string(), { defaultValue: [] }), + throttle: schema.nullable(schema.string({ validate: validateDurationSchema })), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + schedule: schema.object({ + interval: schema.string({ validate: validateDurationSchema }), + }), + actions: schema.arrayOf( + schema.object({ + group: schema.string(), + id: schema.string(), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + { defaultValue: [] } + ), + notify_when: schema.string({ validate: validateNotifyWhenType }), +}); + +const rewriteBodyReq: RewriteRequestCase['data']> = ({ + rule_type_id: alertTypeId, + notify_when: notifyWhen, + ...rest +}) => ({ + ...rest, + alertTypeId, + notifyWhen, +}); +const rewriteBodyRes: RewriteResponseCase> = ({ + actions, + alertTypeId, + scheduledTaskId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus: { lastExecutionDate, ...executionStatus }, + ...rest +}) => ({ + ...rest, + rule_type_id: alertTypeId, + scheduled_task_id: scheduledTaskId, + 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, + execution_status: { + ...executionStatus, + last_execution_date: lastExecutionDate, + }, + actions: actions.map(({ group, id, actionTypeId, params }) => ({ + group, + id, + params, + connector_type_id: actionTypeId, + })), +}); + +export const createRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id?}`, + validate: { + params: schema.maybe( + schema.object({ + id: schema.maybe(schema.string()), + }) + ), + body: bodySchema, + }, + }, + handleDisabledApiKeysError( + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const rule = req.body; + const params = req.params; + try { + const createdRule: SanitizedAlert = await alertsClient.create( + { + data: rewriteBodyReq({ + ...rule, + notify_when: rule.notify_when as AlertNotifyWhenType, + }), + options: { id: params?.id }, + } + ); + return res.ok({ + body: rewriteBodyRes(createdRule), + }); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/delete_rule.test.ts b/x-pack/plugins/alerting/server/routes/delete_rule.test.ts new file mode 100644 index 0000000000000..16d344548fc25 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/delete_rule.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { deleteRuleRoute } from './delete_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('deleteRuleRoute', () => { + it('deletes an alert with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + deleteRuleRoute(router, licenseState); + + const [config, handler] = router.delete.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}"`); + + alertsClient.delete.mockResolvedValueOnce({}); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.delete).toHaveBeenCalledTimes(1); + expect(alertsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the license allows deleting rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + deleteRuleRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + alertsClient.delete.mockResolvedValueOnce({}); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + } + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents deleting rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + deleteRuleRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + alertsClient.delete.mockResolvedValueOnce({}); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + id: '1', + } + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/delete_rule.ts b/x-pack/plugins/alerting/server/routes/delete_rule.ts new file mode 100644 index 0000000000000..724eb5352ed23 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/delete_rule.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState } from '../lib'; +import { verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const deleteRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.delete( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + await alertsClient.delete({ id }); + return res.noContent(); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/disable_rule.test.ts b/x-pack/plugins/alerting/server/routes/disable_rule.test.ts new file mode 100644 index 0000000000000..a77a8443a97fb --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/disable_rule.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { disableRuleRoute } from './disable_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('disableRuleRoute', () => { + it('disables a rule', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + disableRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_disable"`); + + alertsClient.disable.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.disable).toHaveBeenCalledTimes(1); + expect(alertsClient.disable.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + disableRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.disable.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/disable_rule.ts b/x-pack/plugins/alerting/server/routes/disable_rule.ts new file mode 100644 index 0000000000000..2a2f0f4aa25fc --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/disable_rule.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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const disableRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}/_disable`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + try { + await alertsClient.disable({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/enable_rule.test.ts b/x-pack/plugins/alerting/server/routes/enable_rule.test.ts new file mode 100644 index 0000000000000..71889d153ce5f --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/enable_rule.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { enableRuleRoute } from './enable_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('enableRuleRoute', () => { + it('enables a rule', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + enableRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_enable"`); + + alertsClient.enable.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.enable).toHaveBeenCalledTimes(1); + expect(alertsClient.enable.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + enableRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.enable.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/enable_rule.ts b/x-pack/plugins/alerting/server/routes/enable_rule.ts new file mode 100644 index 0000000000000..9c7526630d0a3 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/enable_rule.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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const enableRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}/_enable`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + try { + await alertsClient.enable({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/find_rules.test.ts b/x-pack/plugins/alerting/server/routes/find_rules.test.ts new file mode 100644 index 0000000000000..98bb3c77daecc --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/find_rules.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { findRulesRoute } from './find_rules'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('findRulesRoute', () => { + it('finds rules with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + findRulesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rules/_find"`); + + const findResult = { + page: 1, + perPage: 1, + total: 0, + data: [], + }; + alertsClient.find.mockResolvedValueOnce(findResult); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "data": Array [], + "page": 1, + "per_page": 1, + "total": 0, + }, + } + `); + + expect(alertsClient.find).toHaveBeenCalledTimes(1); + expect(alertsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "options": Object { + "defaultSearchOperator": "OR", + "page": 1, + "perPage": 1, + }, + }, + ] + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + page: 1, + per_page: 1, + total: 0, + data: [], + }, + }); + }); + + it('ensures the license allows finding rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + findRulesRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 0, + data: [], + }); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + } + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents finding rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + findRulesRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + query: { + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + }, + ['ok'] + ); + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts new file mode 100644 index 0000000000000..06b7960b67297 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState } from '../lib'; +import { FindOptions, FindResult } from '../alerts_client'; +import { RewriteRequestCase, RewriteResponseCase, verifyAccessAndContext } from './lib'; +import { AlertTypeParams, AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +// query definition +const querySchema = schema.object({ + per_page: schema.number({ defaultValue: 10, min: 0 }), + page: schema.number({ defaultValue: 1, min: 1 }), + search: schema.maybe(schema.string()), + default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { + defaultValue: 'OR', + }), + search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), + sort_field: schema.maybe(schema.string()), + sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), + has_reference: schema.maybe( + // use nullable as maybe is currently broken + // in config-schema + schema.nullable( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ) + ), + fields: schema.maybe(schema.arrayOf(schema.string())), + filter: schema.maybe(schema.string()), +}); + +const rewriteQueryReq: RewriteRequestCase = ({ + default_search_operator: defaultSearchOperator, + has_reference: hasReference, + search_fields: searchFields, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + ...rest +}) => ({ + ...rest, + defaultSearchOperator, + perPage, + ...(sortField ? { sortField } : {}), + ...(sortOrder ? { sortOrder } : {}), + ...(hasReference ? { hasReference } : {}), + ...(searchFields ? { searchFields } : {}), +}); +const rewriteBodyRes: RewriteResponseCase> = ({ + perPage, + data, + ...restOfResult +}) => { + return { + ...restOfResult, + per_page: perPage, + data: data.map( + ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus, + actions, + scheduledTaskId, + ...rest + }) => ({ + ...rest, + 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 && { + ...omit(executionStatus, 'lastExecutionDate'), + last_execution_date: executionStatus.lastExecutionDate, + }, + actions: actions.map(({ group, id, actionTypeId, params }) => ({ + group, + id, + params, + connector_type_id: actionTypeId, + })), + }) + ), + }; +}; + +export const findRulesRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ALERTING_API_PATH}/rules/_find`, + validate: { + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + + const options = rewriteQueryReq({ + ...req.query, + has_reference: req.query.has_reference || undefined, + search_fields: searchFieldsAsArray(req.query.search_fields), + }); + + const findResult = await alertsClient.find({ options }); + return res.ok({ + body: rewriteBodyRes(findResult), + }); + }) + ) + ); +}; + +function searchFieldsAsArray(searchFields: string | string[] | undefined): string[] | undefined { + if (!searchFields) { + return; + } + return Array.isArray(searchFields) ? searchFields : [searchFields]; +} diff --git a/x-pack/plugins/alerting/server/routes/get_rule.test.ts b/x-pack/plugins/alerting/server/routes/get_rule.test.ts new file mode 100644 index 0000000000000..fc900797cdc89 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { pick } from 'lodash'; +import { getRuleRoute } from './get_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { SanitizedAlert } from '../types'; +import { AsApiContract } from './lib'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getRuleRoute', () => { + const mockedAlert: SanitizedAlert<{ + bar: boolean; + }> = { + id: '1', + alertTypeId: '1', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date(), + updatedAt: new Date(), + actions: [ + { + group: 'default', + id: '2', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + consumer: 'bar', + name: 'abc', + tags: ['foo'], + enabled: true, + muteAll: false, + notifyWhen: 'onActionGroupChange', + createdBy: '', + updatedBy: '', + apiKeyOwner: '', + throttle: '30s', + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + }; + + const getResult: AsApiContract> = { + ...pick(mockedAlert, 'consumer', 'name', 'schedule', 'tags', 'params', 'throttle', 'enabled'), + rule_type_id: mockedAlert.alertTypeId, + notify_when: mockedAlert.notifyWhen, + mute_all: mockedAlert.muteAll, + created_by: mockedAlert.createdBy, + updated_by: mockedAlert.updatedBy, + api_key_owner: mockedAlert.apiKeyOwner, + muted_alert_ids: mockedAlert.mutedInstanceIds, + created_at: mockedAlert.createdAt, + updated_at: mockedAlert.updatedAt, + id: mockedAlert.id, + execution_status: { + status: mockedAlert.executionStatus.status, + last_execution_date: mockedAlert.executionStatus.lastExecutionDate, + }, + actions: [ + { + group: mockedAlert.actions[0].group, + id: mockedAlert.actions[0].id, + params: mockedAlert.actions[0].params, + connector_type_id: mockedAlert.actions[0].actionTypeId, + }, + ], + }; + + it('gets a rule with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleRoute(router, licenseState); + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}"`); + + alertsClient.get.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + await handler(context, req, res); + + expect(alertsClient.get).toHaveBeenCalledTimes(1); + expect(alertsClient.get.mock.calls[0][0].id).toEqual('1'); + + expect(res.ok).toHaveBeenCalledWith({ + body: getResult, + }); + }); + + it('ensures the license allows getting rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.get.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + getRuleRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.get.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts new file mode 100644 index 0000000000000..fd03f32047e74 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { verifyAccessAndContext, RewriteResponseCase } from './lib'; +import { + AlertTypeParams, + AlertingRequestHandlerContext, + BASE_ALERTING_API_PATH, + SanitizedAlert, +} from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const rewriteBodyRes: RewriteResponseCase> = ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus, + actions, + scheduledTaskId, + ...rest +}) => ({ + ...rest, + 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 && { + ...omit(executionStatus, 'lastExecutionDate'), + last_execution_date: executionStatus.lastExecutionDate, + }, + actions: actions.map(({ group, id, actionTypeId, params }) => ({ + group, + id, + params, + connector_type_id: actionTypeId, + })), +}); + +export const getRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + const rule = await alertsClient.get({ id }); + return res.ok({ + body: rewriteBodyRes(rule), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts new file mode 100644 index 0000000000000..fab6d46219a37 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertInstanceSummary } from '../types'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getRuleAlertSummaryRoute', () => { + const dateString = new Date().toISOString(); + const mockedAlertInstanceSummary: AlertInstanceSummary = { + id: '', + name: '', + tags: [], + alertTypeId: '', + consumer: '', + muteAll: false, + throttle: null, + enabled: false, + statusStartDate: dateString, + statusEndDate: dateString, + status: 'OK', + errorMessages: [], + instances: {}, + }; + + it('gets rule alert summary', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleAlertSummaryRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_alert_summary"`); + + alertsClient.getAlertInstanceSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + query: {}, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(alertsClient.getAlertInstanceSummary).toHaveBeenCalledTimes(1); + expect(alertsClient.getAlertInstanceSummary.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "dateStart": undefined, + "id": "1", + }, + ] + `); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('returns NOT-FOUND when rule is not found', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleAlertSummaryRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.getAlertInstanceSummary = jest + .fn() + .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + query: {}, + }, + ['notFound'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts new file mode 100644 index 0000000000000..7a3679851d53f --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState } from '../lib'; +import { GetAlertInstanceSummaryParams } from '../alerts_client'; +import { RewriteRequestCase, RewriteResponseCase, verifyAccessAndContext } from './lib'; +import { + AlertingRequestHandlerContext, + INTERNAL_BASE_ALERTING_API_PATH, + AlertInstanceSummary, +} from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const querySchema = schema.object({ + date_start: schema.maybe(schema.string()), +}); + +const rewriteReq: RewriteRequestCase = ({ + date_start: dateStart, + ...rest +}) => ({ + ...rest, + dateStart, +}); +const rewriteBodyRes: RewriteResponseCase = ({ + alertTypeId, + muteAll, + statusStartDate, + statusEndDate, + errorMessages, + lastRun, + instances: alerts, + ...rest +}) => ({ + ...rest, + alerts, + rule_type_id: alertTypeId, + mute_all: muteAll, + status_start_date: statusStartDate, + status_end_date: statusEndDate, + error_messages: errorMessages, + last_run: lastRun, +}); + +export const getRuleAlertSummaryRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_alert_summary`, + validate: { + params: paramSchema, + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + const summary = await alertsClient.getAlertInstanceSummary( + rewriteReq({ id, ...req.query }) + ); + return res.ok({ body: rewriteBodyRes(summary) }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_state.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_state.test.ts new file mode 100644 index 0000000000000..71e06b60d1026 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_state.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRuleStateRoute } from './get_rule_state'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; +import { alertsClientMock } from '../alerts_client.mock'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getRuleStateRoute', () => { + const mockedAlertState = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date(), + }, + }, + }, + second_instance: {}, + }, + }; + + it('gets rule state', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleStateRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/state"`); + + alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(alertsClient.getAlertState).toHaveBeenCalledTimes(1); + expect(alertsClient.getAlertState.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('returns NO-CONTENT when rule exists but has no task state yet', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleStateRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/state"`); + + alertsClient.getAlertState.mockResolvedValueOnce(undefined); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.getAlertState).toHaveBeenCalledTimes(1); + expect(alertsClient.getAlertState.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('returns NOT-FOUND when rule is not found', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleStateRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/state"`); + + alertsClient.getAlertState = jest + .fn() + .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['notFound'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.getAlertState).toHaveBeenCalledTimes(1); + expect(alertsClient.getAlertState.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_state.ts b/x-pack/plugins/alerting/server/routes/get_rule_state.ts new file mode 100644 index 0000000000000..08087d1ece8af --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_state.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState } from '../lib'; +import { RewriteResponseCase, verifyAccessAndContext } from './lib'; +import { + AlertingRequestHandlerContext, + INTERNAL_BASE_ALERTING_API_PATH, + AlertTaskState, +} from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const rewriteBodyRes: RewriteResponseCase = ({ + alertTypeState, + alertInstances, + previousStartedAt, + ...rest +}) => ({ + ...rest, + rule_type_state: alertTypeState, + alerts: alertInstances, + previous_started_at: previousStartedAt, +}); + +export const getRuleStateRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/state`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + const state = await alertsClient.getAlertState({ id }); + return state ? res.ok({ body: rewriteBodyRes(state) }) : res.noContent(); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 75c621e4a0abf..be63e0b7054be 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -54,7 +54,7 @@ describe('healthRoute', () => { const [config] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_health"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/_health"`); }); it('queries the usage api', async () => { @@ -107,22 +107,22 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toStrictEqual({ body: { - alertingFrameworkHeath: { - decryptionHealth: { + alerting_framework_heath: { + decryption_health: { status: HealthStatus.OK, timestamp: currentDate, }, - executionHealth: { + execution_health: { status: HealthStatus.OK, timestamp: currentDate, }, - readHealth: { + read_health: { status: HealthStatus.OK, timestamp: currentDate, }, }, - hasPermanentEncryptionKey: false, - isSufficientlySecure: true, + has_permanent_encryption_key: false, + is_sufficiently_secure: true, }, }); }); @@ -148,22 +148,22 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toStrictEqual({ body: { - alertingFrameworkHeath: { - decryptionHealth: { + alerting_framework_heath: { + decryption_health: { status: HealthStatus.OK, timestamp: currentDate, }, - executionHealth: { + execution_health: { status: HealthStatus.OK, timestamp: currentDate, }, - readHealth: { + read_health: { status: HealthStatus.OK, timestamp: currentDate, }, }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: true, + has_permanent_encryption_key: true, + is_sufficiently_secure: true, }, }); }); @@ -189,22 +189,22 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toStrictEqual({ body: { - alertingFrameworkHeath: { - decryptionHealth: { + alerting_framework_heath: { + decryption_health: { status: HealthStatus.OK, timestamp: currentDate, }, - executionHealth: { + execution_health: { status: HealthStatus.OK, timestamp: currentDate, }, - readHealth: { + read_health: { status: HealthStatus.OK, timestamp: currentDate, }, }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: true, + has_permanent_encryption_key: true, + is_sufficiently_secure: true, }, }); }); @@ -230,22 +230,22 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toStrictEqual({ body: { - alertingFrameworkHeath: { - decryptionHealth: { + alerting_framework_heath: { + decryption_health: { status: HealthStatus.OK, timestamp: currentDate, }, - executionHealth: { + execution_health: { status: HealthStatus.OK, timestamp: currentDate, }, - readHealth: { + read_health: { status: HealthStatus.OK, timestamp: currentDate, }, }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: false, + has_permanent_encryption_key: true, + is_sufficiently_secure: false, }, }); }); @@ -273,22 +273,22 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toStrictEqual({ body: { - alertingFrameworkHeath: { - decryptionHealth: { + alerting_framework_heath: { + decryption_health: { status: HealthStatus.OK, timestamp: currentDate, }, - executionHealth: { + execution_health: { status: HealthStatus.OK, timestamp: currentDate, }, - readHealth: { + read_health: { status: HealthStatus.OK, timestamp: currentDate, }, }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: false, + has_permanent_encryption_key: true, + is_sufficiently_secure: false, }, }); }); @@ -316,22 +316,22 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toStrictEqual({ body: { - alertingFrameworkHeath: { - decryptionHealth: { + alerting_framework_heath: { + decryption_health: { status: HealthStatus.OK, timestamp: currentDate, }, - executionHealth: { + execution_health: { status: HealthStatus.OK, timestamp: currentDate, }, - readHealth: { + read_health: { status: HealthStatus.OK, timestamp: currentDate, }, }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: true, + has_permanent_encryption_key: true, + is_sufficiently_secure: true, }, }); }); diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index de0b14465c5ac..c2a122a28fa49 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -6,11 +6,15 @@ */ import { ApiResponse } from '@elastic/elasticsearch'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { AlertingFrameworkHealth } from '../types'; +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +import { RewriteResponseCase, verifyAccessAndContext } from './lib'; +import { + AlertingRequestHandlerContext, + BASE_ALERTING_API_PATH, + AlertingFrameworkHealth, +} from '../types'; interface XPackUsageSecurity { security?: { @@ -23,49 +27,63 @@ interface XPackUsageSecurity { }; } -export function healthRoute( - router: AlertingRouter, +const rewriteBodyRes: RewriteResponseCase = ({ + isSufficientlySecure, + hasPermanentEncryptionKey, + alertingFrameworkHeath, + ...rest +}) => ({ + ...rest, + is_sufficiently_secure: isSufficientlySecure, + has_permanent_encryption_key: hasPermanentEncryptionKey, + alerting_framework_heath: { + decryption_health: alertingFrameworkHeath.decryptionHealth, + execution_health: alertingFrameworkHeath.executionHealth, + read_health: alertingFrameworkHeath.readHealth, + }, +}); + +export const healthRoute = ( + router: IRouter, licenseState: ILicenseState, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -) { +) => { router.get( { - path: '/api/alerts/_health', + path: `${BASE_ALERTING_API_PATH}/_health`, validate: false, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - try { - const { - body: { - security: { - enabled: isSecurityEnabled = false, - ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, - } = {}, - }, - }: ApiResponse = await context.core.elasticsearch.client.asInternalUser.transport // Do not augment with such input. // `transport.request` is potentially unsafe when combined with untrusted user input. - .request({ - method: 'GET', - path: '/_xpack/usage', - }); + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + try { + const { + body: { + security: { + enabled: isSecurityEnabled = false, + ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, + } = {}, + }, + }: ApiResponse = await context.core.elasticsearch.client.asInternalUser.transport // Do not augment with such input. // `transport.request` is potentially unsafe when combined with untrusted user input. + .request({ + method: 'GET', + path: '/_xpack/usage', + }); - const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); + const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); - const frameworkHealth: AlertingFrameworkHealth = { - isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), - hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, - alertingFrameworkHeath, - }; + const frameworkHealth: AlertingFrameworkHealth = { + isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), + hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + alertingFrameworkHeath, + }; - return res.ok({ - body: frameworkHealth, - }); - } catch (error) { - return res.badRequest({ body: error }); - } - }) + return res.ok({ + body: rewriteBodyRes(frameworkHealth), + }); + } catch (error) { + return res.badRequest({ body: error }); + } + }) + ) ); -} +}; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index e250d17866a2b..c6f12ffba2f20 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -5,20 +5,50 @@ * 2.0. */ -export { aggregateAlertRoute } from './aggregate'; -export { createAlertRoute } from './create'; -export { deleteAlertRoute } from './delete'; -export { findAlertRoute } from './find'; -export { getAlertRoute } from './get'; -export { getAlertStateRoute } from './get_alert_state'; -export { getAlertInstanceSummaryRoute } from './get_alert_instance_summary'; -export { listAlertTypesRoute } from './list_alert_types'; -export { updateAlertRoute } from './update'; -export { enableAlertRoute } from './enable'; -export { disableAlertRoute } from './disable'; -export { updateApiKeyRoute } from './update_api_key'; -export { muteAlertInstanceRoute } from './mute_instance'; -export { unmuteAlertInstanceRoute } from './unmute_instance'; -export { muteAllAlertRoute } from './mute_all'; -export { unmuteAllAlertRoute } from './unmute_all'; -export { healthRoute } from './health'; +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { defineLegacyRoutes } from './legacy'; +import { AlertingRequestHandlerContext } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +import { createRuleRoute } from './create_rule'; +import { getRuleRoute } from './get_rule'; +import { updateRuleRoute } from './update_rule'; +import { deleteRuleRoute } from './delete_rule'; +import { aggregateRulesRoute } from './aggregate_rules'; +import { disableRuleRoute } from './disable_rule'; +import { enableRuleRoute } from './enable_rule'; +import { findRulesRoute } from './find_rules'; +import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; +import { getRuleStateRoute } from './get_rule_state'; +import { healthRoute } from './health'; +import { ruleTypesRoute } from './rule_types'; +import { muteAllRuleRoute } from './mute_all_rule'; +import { muteAlertRoute } from './mute_alert'; +import { unmuteAllRuleRoute } from './unmute_all_rule'; +import { unmuteAlertRoute } from './unmute_alert'; +import { updateRuleApiKeyRoute } from './update_rule_api_key'; + +export function defineRoutes( + router: IRouter, + licenseState: ILicenseState, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + defineLegacyRoutes(router, licenseState, encryptedSavedObjects); + createRuleRoute(router, licenseState); + getRuleRoute(router, licenseState); + updateRuleRoute(router, licenseState); + deleteRuleRoute(router, licenseState); + aggregateRulesRoute(router, licenseState); + disableRuleRoute(router, licenseState); + enableRuleRoute(router, licenseState); + findRulesRoute(router, licenseState); + getRuleAlertSummaryRoute(router, licenseState); + getRuleStateRoute(router, licenseState); + healthRoute(router, licenseState, encryptedSavedObjects); + ruleTypesRoute(router, licenseState); + muteAllRuleRoute(router, licenseState); + muteAlertRoute(router, licenseState); + unmuteAllRuleRoute(router, licenseState); + unmuteAlertRoute(router, licenseState); + updateRuleApiKeyRoute(router, licenseState); +} diff --git a/x-pack/plugins/alerting/server/routes/aggregate.test.ts b/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts similarity index 91% rename from x-pack/plugins/alerting/server/routes/aggregate.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts index 60e9a2a1bcc79..94331902d9ace 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts @@ -7,14 +7,14 @@ import { aggregateAlertRoute } from './aggregate'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/aggregate.ts b/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts similarity index 83% rename from x-pack/plugins/alerting/server/routes/aggregate.ts rename to x-pack/plugins/alerting/server/routes/legacy/aggregate.ts index 1416f60277daa..91189fdf3d0a6 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts @@ -6,12 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { renameKeys } from './lib/rename_keys'; -import { FindOptions } from '../alerts_client'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { renameKeys } from './../lib/rename_keys'; +import { FindOptions } from '../../alerts_client'; // config definition const querySchema = schema.object({ @@ -36,7 +36,7 @@ const querySchema = schema.object({ export const aggregateAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/_aggregate`, + path: `${LEGACY_BASE_ALERT_API_PATH}/_aggregate`, validate: { query: querySchema, }, diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/legacy/create.test.ts similarity index 93% rename from x-pack/plugins/alerting/server/routes/create.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/create.test.ts index 1a2dfec8612e3..fd3252d2fca77 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/create.test.ts @@ -7,16 +7,16 @@ import { createAlertRoute } from './create'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { Alert } from '../../common/alert'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { Alert } from '../../../common/alert'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerting/server/routes/legacy/create.ts similarity index 75% rename from x-pack/plugins/alerting/server/routes/create.ts rename to x-pack/plugins/alerting/server/routes/legacy/create.ts index 7b1a518112ddd..fca2b67118527 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/create.ts @@ -6,19 +6,19 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { validateDurationSchema } from '../lib'; -import { handleDisabledApiKeysError } from './lib/error_handler'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { validateDurationSchema } from '../../lib'; +import { handleDisabledApiKeysError } from './../lib/error_handler'; import { - Alert, + SanitizedAlert, AlertNotifyWhenType, AlertTypeParams, - BASE_ALERT_API_PATH, + LEGACY_BASE_ALERT_API_PATH, validateNotifyWhenType, -} from '../types'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +} from '../../types'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; export const bodySchema = schema.object({ name: schema.string(), @@ -46,7 +46,7 @@ export const bodySchema = schema.object({ export const createAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{id?}`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id?}`, validate: { params: schema.maybe( schema.object({ @@ -68,10 +68,12 @@ export const createAlertRoute = (router: AlertingRouter, licenseState: ILicenseS const params = req.params; const notifyWhen = alert?.notifyWhen ? (alert.notifyWhen as AlertNotifyWhenType) : null; try { - const alertRes: Alert = await alertsClient.create({ - data: { ...alert, notifyWhen }, - options: { id: params?.id }, - }); + const alertRes: SanitizedAlert = await alertsClient.create( + { + data: { ...alert, notifyWhen }, + options: { id: params?.id }, + } + ); return res.ok({ body: alertRes, }); diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/legacy/delete.test.ts similarity index 89% rename from x-pack/plugins/alerting/server/routes/delete.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/delete.test.ts index 47564f67d42a5..e71b2788b98c7 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/delete.test.ts @@ -7,14 +7,14 @@ import { deleteAlertRoute } from './delete'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerting/server/routes/legacy/delete.ts similarity index 76% rename from x-pack/plugins/alerting/server/routes/delete.ts rename to x-pack/plugins/alerting/server/routes/legacy/delete.ts index e217fd0533771..650126be4499d 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/delete.ts @@ -6,10 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; const paramSchema = schema.object({ id: schema.string(), @@ -18,7 +18,7 @@ const paramSchema = schema.object({ export const deleteAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.delete( { - path: `${BASE_ALERT_API_PATH}/alert/{id}`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/legacy/disable.test.ts similarity index 86% rename from x-pack/plugins/alerting/server/routes/disable.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/disable.test.ts index 347b1641515e6..34c0af711a338 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/disable.test.ts @@ -7,14 +7,14 @@ import { disableAlertRoute } from './disable'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerting/server/routes/legacy/disable.ts similarity index 74% rename from x-pack/plugins/alerting/server/routes/disable.ts rename to x-pack/plugins/alerting/server/routes/legacy/disable.ts index 7129fbdd52698..140e0492fbaab 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/disable.ts @@ -6,11 +6,11 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -19,7 +19,7 @@ const paramSchema = schema.object({ export const disableAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/_disable`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_disable`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/legacy/enable.test.ts similarity index 86% rename from x-pack/plugins/alerting/server/routes/enable.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/enable.test.ts index c12a19fc35dbe..88229472a2936 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/enable.test.ts @@ -7,14 +7,14 @@ import { enableAlertRoute } from './enable'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerting/server/routes/legacy/enable.ts similarity index 72% rename from x-pack/plugins/alerting/server/routes/enable.ts rename to x-pack/plugins/alerting/server/routes/legacy/enable.ts index d874e9b6106b9..fcf9ceb8a058b 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/enable.ts @@ -6,12 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { handleDisabledApiKeysError } from './lib/error_handler'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { handleDisabledApiKeysError } from './../lib/error_handler'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -20,7 +20,7 @@ const paramSchema = schema.object({ export const enableAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/_enable`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_enable`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/legacy/find.test.ts similarity index 91% rename from x-pack/plugins/alerting/server/routes/find.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/find.test.ts index 791e2babc062e..640451afcca97 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/find.test.ts @@ -7,14 +7,14 @@ import { findAlertRoute } from './find'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerting/server/routes/legacy/find.ts similarity index 86% rename from x-pack/plugins/alerting/server/routes/find.ts rename to x-pack/plugins/alerting/server/routes/legacy/find.ts index ad345de685266..1d54df53d883a 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/find.ts @@ -6,13 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; +import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { renameKeys } from './lib/rename_keys'; -import { FindOptions } from '../alerts_client'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { renameKeys } from './../lib/rename_keys'; +import { FindOptions } from '../../alerts_client'; // config definition const querySchema = schema.object({ @@ -42,7 +42,7 @@ const querySchema = schema.object({ export const findAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/_find`, + path: `${LEGACY_BASE_ALERT_API_PATH}/_find`, validate: { query: querySchema, }, diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get.test.ts similarity index 90% rename from x-pack/plugins/alerting/server/routes/get.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/get.test.ts index 79938e572dfcf..b2704cdd234f1 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get.test.ts @@ -7,14 +7,14 @@ import { getAlertRoute } from './get'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { Alert } from '../../common'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { Alert } from '../../../common'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerting/server/routes/legacy/get.ts similarity index 76% rename from x-pack/plugins/alerting/server/routes/get.ts rename to x-pack/plugins/alerting/server/routes/legacy/get.ts index 93d2f29f53c18..cf63a4387a93e 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get.ts @@ -6,10 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import type { AlertingRouter } from '../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import type { AlertingRouter } from '../../types'; const paramSchema = schema.object({ id: schema.string(), @@ -18,7 +18,7 @@ const paramSchema = schema.object({ export const getAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/alert/{id}`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts similarity index 89% rename from x-pack/plugins/alerting/server/routes/get_alert_instance_summary.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts index 3157f18afbb95..0162ef91f55e7 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts @@ -7,14 +7,14 @@ import { getAlertInstanceSummaryRoute } from './get_alert_instance_summary'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertInstanceSummary } from '../types'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertInstanceSummary } from '../../types'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_instance_summary.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts similarity index 79% rename from x-pack/plugins/alerting/server/routes/get_alert_instance_summary.ts rename to x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts index 71c85caa38c8d..00c96197f6f9b 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_instance_summary.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts @@ -6,10 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; const paramSchema = schema.object({ id: schema.string(), @@ -25,7 +25,7 @@ export const getAlertInstanceSummaryRoute = ( ) => { router.get( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/_instance_summary`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_instance_summary`, validate: { params: paramSchema, query: querySchema, diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.test.ts similarity index 93% rename from x-pack/plugins/alerting/server/routes/get_alert_state.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/get_alert_state.test.ts index 5aa96d6c73447..f08e0992cbba0 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.test.ts @@ -7,13 +7,13 @@ import { getAlertStateRoute } from './get_alert_state'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; -import { alertsClientMock } from '../alerts_client.mock'; +import { alertsClientMock } from '../../alerts_client.mock'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts similarity index 77% rename from x-pack/plugins/alerting/server/routes/get_alert_state.ts rename to x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts index 3a05946162d37..5e7cbfbe6eb95 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts @@ -6,10 +6,10 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; const paramSchema = schema.object({ id: schema.string(), @@ -18,7 +18,7 @@ const paramSchema = schema.object({ export const getAlertStateRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/state`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/state`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.test.ts b/x-pack/plugins/alerting/server/routes/legacy/health.test.ts new file mode 100644 index 0000000000000..74de5f70a32e7 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/legacy/health.test.ts @@ -0,0 +1,336 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { healthRoute } from './health'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { HealthStatus } from '../../types'; +import { alertsMock } from '../../mocks'; +const alertsClient = alertsClientMock.create(); + +jest.mock('../../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +const alerting = alertsMock.createStart(); + +const currentDate = new Date().toISOString(); +beforeEach(() => { + jest.resetAllMocks(); + alerting.getFrameworkHealth.mockResolvedValue({ + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }); +}); + +describe('healthRoute', () => { + it('registers the route', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + + const [config] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_health"`); + }); + + it('queries the usage api', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({}) + ); + + const [context, req, res] = mockHandlerArguments({ esClient, alertsClient }, {}, ['ok']); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + + expect(esClient.asInternalUser.transport.request.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "method": "GET", + "path": "/_xpack/usage", + }, + ] + `); + }); + + it('evaluates whether Encrypted Saved Objects is missing encryption key', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: false }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({}) + ); + + const [context, req, res] = mockHandlerArguments( + { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, + {}, + ['ok'] + ); + + expect(await handler(context, req, res)).toStrictEqual({ + body: { + alertingFrameworkHeath: { + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }, + hasPermanentEncryptionKey: false, + isSufficientlySecure: true, + }, + }); + }); + + it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({}) + ); + + const [context, req, res] = mockHandlerArguments( + { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, + {}, + ['ok'] + ); + + expect(await handler(context, req, res)).toStrictEqual({ + body: { + alertingFrameworkHeath: { + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: true, + }, + }); + }); + + it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({}) + ); + + const [context, req, res] = mockHandlerArguments( + { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, + {}, + ['ok'] + ); + + expect(await handler(context, req, res)).toStrictEqual({ + body: { + alertingFrameworkHeath: { + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: true, + }, + }); + }); + + it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ security: { enabled: true } }) + ); + + const [context, req, res] = mockHandlerArguments( + { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, + {}, + ['ok'] + ); + + expect(await handler(context, req, res)).toStrictEqual({ + body: { + alertingFrameworkHeath: { + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: false, + }, + }); + }); + + it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + security: { enabled: true, ssl: {} }, + }) + ); + + const [context, req, res] = mockHandlerArguments( + { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, + {}, + ['ok'] + ); + + expect(await handler(context, req, res)).toStrictEqual({ + body: { + alertingFrameworkHeath: { + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: false, + }, + }); + }); + + it('evaluates security and tls enabled to mean that the user can generate keys', async () => { + const router = httpServiceMock.createRouter(); + + const licenseState = licenseStateMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + security: { enabled: true, ssl: { http: { enabled: true } } }, + }) + ); + + const [context, req, res] = mockHandlerArguments( + { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, + {}, + ['ok'] + ); + + expect(await handler(context, req, res)).toStrictEqual({ + body: { + alertingFrameworkHeath: { + decryptionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + executionHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + readHealth: { + status: HealthStatus.OK, + timestamp: currentDate, + }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: true, + }, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.ts b/x-pack/plugins/alerting/server/routes/legacy/health.ts new file mode 100644 index 0000000000000..b9906a56ce972 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/legacy/health.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApiResponse } from '@elastic/elasticsearch'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { AlertingFrameworkHealth } from '../../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; + +interface XPackUsageSecurity { + security?: { + enabled?: boolean; + ssl?: { + http?: { + enabled?: boolean; + }; + }; + }; +} + +export function healthRoute( + router: AlertingRouter, + licenseState: ILicenseState, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + router.get( + { + path: '/api/alerts/_health', + validate: false, + }, + router.handleLegacyErrors(async function (context, req, res) { + verifyApiAccess(licenseState); + if (!context.alerting) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); + } + try { + const { + body: { + security: { + enabled: isSecurityEnabled = false, + ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, + } = {}, + }, + }: ApiResponse = await context.core.elasticsearch.client.asInternalUser.transport // Do not augment with such input. // `transport.request` is potentially unsafe when combined with untrusted user input. + .request({ + method: 'GET', + path: '/_xpack/usage', + }); + + const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); + + const frameworkHealth: AlertingFrameworkHealth = { + isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), + hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + alertingFrameworkHeath, + }; + + return res.ok({ + body: frameworkHealth, + }); + } catch (error) { + return res.badRequest({ body: error }); + } + }) + ); +} diff --git a/x-pack/plugins/alerting/server/routes/legacy/index.ts b/x-pack/plugins/alerting/server/routes/legacy/index.ts new file mode 100644 index 0000000000000..d1b2f9784f24d --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/legacy/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../../lib'; +import { AlertingRequestHandlerContext } from '../../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; + +import { aggregateAlertRoute } from './aggregate'; +import { createAlertRoute } from './create'; +import { deleteAlertRoute } from './delete'; +import { findAlertRoute } from './find'; +import { getAlertRoute } from './get'; +import { getAlertStateRoute } from './get_alert_state'; +import { getAlertInstanceSummaryRoute } from './get_alert_instance_summary'; +import { listAlertTypesRoute } from './list_alert_types'; +import { updateAlertRoute } from './update'; +import { enableAlertRoute } from './enable'; +import { disableAlertRoute } from './disable'; +import { updateApiKeyRoute } from './update_api_key'; +import { muteAlertInstanceRoute } from './mute_instance'; +import { unmuteAlertInstanceRoute } from './unmute_instance'; +import { muteAllAlertRoute } from './mute_all'; +import { unmuteAllAlertRoute } from './unmute_all'; +import { healthRoute } from './health'; + +export function defineLegacyRoutes( + router: IRouter, + licenseState: ILicenseState, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + aggregateAlertRoute(router, licenseState); + createAlertRoute(router, licenseState); + deleteAlertRoute(router, licenseState); + findAlertRoute(router, licenseState); + getAlertRoute(router, licenseState); + getAlertStateRoute(router, licenseState); + getAlertInstanceSummaryRoute(router, licenseState); + listAlertTypesRoute(router, licenseState); + updateAlertRoute(router, licenseState); + enableAlertRoute(router, licenseState); + disableAlertRoute(router, licenseState); + updateApiKeyRoute(router, licenseState); + muteAllAlertRoute(router, licenseState); + unmuteAllAlertRoute(router, licenseState); + muteAlertInstanceRoute(router, licenseState); + unmuteAlertInstanceRoute(router, licenseState); + healthRoute(router, licenseState, encryptedSavedObjects); +} diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts similarity index 92% rename from x-pack/plugins/alerting/server/routes/list_alert_types.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts index 5ef2e8cc9ec7d..3e6f2f484a6d8 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts @@ -7,16 +7,16 @@ import { listAlertTypesRoute } from './list_alert_types'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { RecoveredActionGroup } from '../../common'; -import { RegistryAlertTypeWithAuth } from '../authorization'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { RecoveredActionGroup } from '../../../common'; +import { RegistryAlertTypeWithAuth } from '../../authorization'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts similarity index 72% rename from x-pack/plugins/alerting/server/routes/list_alert_types.ts rename to x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts index 2040f71f8ad05..da41cc487228c 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts @@ -5,15 +5,15 @@ * 2.0. */ -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; export const listAlertTypesRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/list_alert_types`, + path: `${LEGACY_BASE_ALERT_API_PATH}/list_alert_types`, validate: {}, }, router.handleLegacyErrors(async function (context, req, res) { diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_all.test.ts similarity index 86% rename from x-pack/plugins/alerting/server/routes/mute_all.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/mute_all.test.ts index 8c938d71ab06c..ffe893fa60490 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/mute_all.test.ts @@ -7,13 +7,13 @@ import { muteAllAlertRoute } from './mute_all'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts similarity index 74% rename from x-pack/plugins/alerting/server/routes/mute_all.ts rename to x-pack/plugins/alerting/server/routes/legacy/mute_all.ts index fee1fdfe5a4f7..643aeb97084a8 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts @@ -6,11 +6,11 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -19,7 +19,7 @@ const paramSchema = schema.object({ export const muteAllAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/_mute_all`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_mute_all`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.test.ts similarity index 87% rename from x-pack/plugins/alerting/server/routes/mute_instance.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/mute_instance.test.ts index 83bf57f5259a3..a00c75663c5f0 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.test.ts @@ -7,13 +7,13 @@ import { muteAlertInstanceRoute } from './mute_instance'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts similarity index 72% rename from x-pack/plugins/alerting/server/routes/mute_instance.ts rename to x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts index ab7749178f6cf..2b35f59c7fce1 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts @@ -6,13 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { renameKeys } from './lib/rename_keys'; -import { MuteOptions } from '../alerts_client'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { renameKeys } from './../lib/rename_keys'; +import { MuteOptions } from '../../alerts_client'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ alert_id: schema.string(), @@ -22,7 +22,7 @@ const paramSchema = schema.object({ export const muteAlertInstanceRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.test.ts similarity index 86% rename from x-pack/plugins/alerting/server/routes/unmute_all.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/unmute_all.test.ts index 0c2d3105e581c..8511a8b68a447 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.test.ts @@ -7,13 +7,13 @@ import { unmuteAllAlertRoute } from './unmute_all'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts similarity index 74% rename from x-pack/plugins/alerting/server/routes/unmute_all.ts rename to x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts index 2e67884f8a944..1259428be3329 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts @@ -6,11 +6,11 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -19,7 +19,7 @@ const paramSchema = schema.object({ export const unmuteAllAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/_unmute_all`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_unmute_all`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.test.ts similarity index 87% rename from x-pack/plugins/alerting/server/routes/unmute_instance.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/unmute_instance.test.ts index eddd00390a7f3..d28868a3c9230 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.test.ts @@ -7,13 +7,13 @@ import { unmuteAlertInstanceRoute } from './unmute_instance'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts similarity index 74% rename from x-pack/plugins/alerting/server/routes/unmute_instance.ts rename to x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts index d39fc696eef08..25f5aa654bde1 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts @@ -6,11 +6,11 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ alertId: schema.string(), @@ -20,7 +20,7 @@ const paramSchema = schema.object({ export const unmuteAlertInstanceRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/legacy/update.test.ts similarity index 92% rename from x-pack/plugins/alerting/server/routes/update.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/update.test.ts index 892f1a25844c4..da58d0dc41522 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update.test.ts @@ -7,15 +7,15 @@ import { updateAlertRoute } from './update'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; -import { AlertNotifyWhenType } from '../../common'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; +import { AlertNotifyWhenType } from '../../../common'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerting/server/routes/legacy/update.ts similarity index 81% rename from x-pack/plugins/alerting/server/routes/update.ts rename to x-pack/plugins/alerting/server/routes/legacy/update.ts index 11373e71d14be..23a0719319e00 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update.ts @@ -6,13 +6,17 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { validateDurationSchema } from '../lib'; -import { handleDisabledApiKeysError } from './lib/error_handler'; -import { AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../../common'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { validateDurationSchema } from '../../lib'; +import { handleDisabledApiKeysError } from './../lib/error_handler'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; +import { + AlertNotifyWhenType, + LEGACY_BASE_ALERT_API_PATH, + validateNotifyWhenType, +} from '../../../common'; const paramSchema = schema.object({ id: schema.string(), @@ -41,7 +45,7 @@ const bodySchema = schema.object({ export const updateAlertRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.put( { - path: `${BASE_ALERT_API_PATH}/alert/{id}`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}`, validate: { body: bodySchema, params: paramSchema, diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts similarity index 86% rename from x-pack/plugins/alerting/server/routes/update_api_key.test.ts rename to x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts index 0added369fd61..3f556f480f69c 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts @@ -7,13 +7,13 @@ import { updateApiKeyRoute } from './update_api_key'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { alertsClientMock } from '../alerts_client.mock'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './../_mock_handler_arguments'; +import { alertsClientMock } from '../../alerts_client.mock'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts similarity index 72% rename from x-pack/plugins/alerting/server/routes/update_api_key.ts rename to x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts index a615ee8a5bbd2..a4da2538b0bf9 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts @@ -6,12 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -import type { AlertingRouter } from '../types'; -import { ILicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { BASE_ALERT_API_PATH } from '../../common'; -import { handleDisabledApiKeysError } from './lib/error_handler'; -import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import type { AlertingRouter } from '../../types'; +import { ILicenseState } from '../../lib/license_state'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { handleDisabledApiKeysError } from './../lib/error_handler'; +import { AlertTypeDisabledError } from '../../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -20,7 +20,7 @@ const paramSchema = schema.object({ export const updateApiKeyRoute = (router: AlertingRouter, licenseState: ILicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/alert/{id}/_update_api_key`, + path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_update_api_key`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/lib/index.ts b/x-pack/plugins/alerting/server/routes/lib/index.ts new file mode 100644 index 0000000000000..142513e23e5e7 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/lib/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + handleDisabledApiKeysError, + isApiKeyDisabledError, + isSecurityPluginDisabledError, +} from './error_handler'; +export { renameKeys } from './rename_keys'; +export { AsApiContract, RewriteRequestCase, RewriteResponseCase } from './rewrite_request_case'; +export { verifyAccessAndContext } from './verify_access_and_context'; diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts new file mode 100644 index 0000000000000..361ba5ff5e55d --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { JsonValue } from '../../../../../../src/plugins/kibana_utils/common'; + +type RenameAlertToRule = K extends `alertTypeId` + ? `ruleTypeId` + : K extends `alertId` + ? `ruleId` + : K extends `alertExecutionStatus` + ? `ruleExecutionStatus` + : K extends `actionTypeId` + ? `connectorTypeId` + : K extends `alertInstanceId` + ? `alertId` + : K extends `mutedInstanceIds` + ? `mutedAlertIds` + : K extends `instances` + ? `alerts` + : K; + +export type AsApiContract< + T, + ComplexPropertyKeys = `actions` | `executionStatus`, + OpaquePropertyKeys = `params` +> = T extends Array + ? Array> + : { + [K in keyof T as CamelToSnake< + RenameAlertToRule> + >]: K extends OpaquePropertyKeys + ? // don't convert explciitly opaque types which we treat as a black box + T[K] + : T[K] extends undefined + ? AsApiContract> | undefined + : // don't convert built in types + T[K] extends Date | JsonValue + ? T[K] + : T[K] extends Array + ? Array> + : K extends ComplexPropertyKeys + ? AsApiContract + : T[K] extends object + ? AsApiContract + : // don't convert anything else + T[K]; + }; + +export type RewriteRequestCase = (requested: AsApiContract) => T; +export type RewriteResponseCase = ( + responded: T +) => T extends Array ? Array> : AsApiContract; + +/** + * This type maps Camel Case strings into their Snake Case version. + * This is achieved by checking each character and, if it is an uppercase character, it is mapped to an + * underscore followed by a lowercase one. + * + * The reason there are two ternaries is that, for perfformance reasons, TS limits its + * character parsing to ~15 characters. + * To get around this we use the second turnery to parse 2 characters at a time, which allows us to support + * strings that are 30 characters long. + * + * If you get the TS #2589 error ("Type instantiation is excessively deep and possibly infinite") then most + * likely you have a string that's longer than 30 characters. + * Address this by reducing the length if possible, otherwise, you'll need to add a 3rd ternary which + * parses 3 chars at a time :grimace: + * + * For more details see this PR comment: https://github.com/microsoft/TypeScript/pull/40336#issuecomment-686723087 + */ +type CamelToSnake = string extends T + ? string + : T extends `${infer C0}${infer C1}${infer R}` + ? `${C0 extends Uppercase ? '_' : ''}${Lowercase}${C1 extends Uppercase + ? '_' + : ''}${Lowercase}${CamelToSnake}` + : T extends `${infer C0}${infer R}` + ? `${C0 extends Uppercase ? '_' : ''}${Lowercase}${CamelToSnake}` + : ''; diff --git a/x-pack/plugins/alerting/server/routes/lib/verify_access_and_context.ts b/x-pack/plugins/alerting/server/routes/lib/verify_access_and_context.ts new file mode 100644 index 0000000000000..f0177f04bf9b2 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/lib/verify_access_and_context.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandler } from 'kibana/server'; +import { ILicenseState, isErrorThatHandlesItsOwnResponse, verifyApiAccess } from '../../lib'; +import { AlertingRequestHandlerContext } from '../../types'; + +type AlertingRequestHandlerWrapper = ( + licenseState: ILicenseState, + handler: RequestHandler +) => RequestHandler; + +export const verifyAccessAndContext: AlertingRequestHandlerWrapper = (licenseState, handler) => { + return async (context, request, response) => { + verifyApiAccess(licenseState); + + if (!context.alerting) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); + } + + try { + return await handler(context, request, response); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(response); + } + throw e; + } + }; +}; diff --git a/x-pack/plugins/alerting/server/routes/mute_alert.test.ts b/x-pack/plugins/alerting/server/routes/mute_alert.test.ts new file mode 100644 index 0000000000000..64ba22f2980ec --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/mute_alert.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { muteAlertRoute } from './mute_alert'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('muteAlertRoute', () => { + it('mutes an alert', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + muteAlertRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot( + `"/api/alerting/rule/{rule_id}/alert/{alert_id}/_mute"` + ); + + alertsClient.muteInstance.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + rule_id: '1', + alert_id: '2', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.muteInstance).toHaveBeenCalledTimes(1); + expect(alertsClient.muteInstance.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "alertId": "1", + "alertInstanceId": "2", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + muteAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.muteInstance.mockRejectedValue( + new AlertTypeDisabledError('Fail', 'license_invalid') + ); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/mute_alert.ts b/x-pack/plugins/alerting/server/routes/mute_alert.ts new file mode 100644 index 0000000000000..f1b928cf8c543 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/mute_alert.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { MuteOptions } from '../alerts_client'; +import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + rule_id: schema.string(), + alert_id: schema.string(), +}); + +const rewriteParamsReq: RewriteRequestCase = ({ + rule_id: alertId, + alert_id: alertInstanceId, +}) => ({ + alertId, + alertInstanceId, +}); + +export const muteAlertRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{rule_id}/alert/{alert_id}/_mute`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const params = rewriteParamsReq(req.params); + try { + await alertsClient.muteInstance(params); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/mute_all_rule.test.ts b/x-pack/plugins/alerting/server/routes/mute_all_rule.test.ts new file mode 100644 index 0000000000000..0d53708db2567 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/mute_all_rule.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { muteAllRuleRoute } from './mute_all_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('muteAllRuleRoute', () => { + it('mute a rule', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + muteAllRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_mute_all"`); + + alertsClient.muteAll.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.muteAll).toHaveBeenCalledTimes(1); + expect(alertsClient.muteAll.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + muteAllRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.muteAll.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/mute_all_rule.ts b/x-pack/plugins/alerting/server/routes/mute_all_rule.ts new file mode 100644 index 0000000000000..29d40249ef079 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/mute_all_rule.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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const muteAllRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}/_mute_all`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + try { + await alertsClient.muteAll({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts new file mode 100644 index 0000000000000..58c9a4b4c46fd --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { ruleTypesRoute } from './rule_types'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { RecoveredActionGroup } from '../../common'; +import { RegistryAlertTypeWithAuth } from '../authorization'; +import { AsApiContract } from './lib'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('ruleTypesRoute', () => { + it('lists rule types with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + ruleTypesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule_types"`); + + const listTypes = [ + { + id: '1', + name: 'name', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + authorizedConsumers: {}, + actionVariables: { + context: [], + state: [], + }, + producer: 'test', + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, + ]; + const expectedResult: Array> = [ + { + id: '1', + name: 'name', + action_groups: [ + { + id: 'default', + name: 'Default', + }, + ], + default_action_group_id: 'default', + minimum_license_required: 'basic', + recovery_action_group: RecoveredActionGroup, + authorized_consumers: {}, + action_variables: { + context: [], + state: [], + }, + producer: 'test', + enabled_in_license: true, + }, + ]; + alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Array [ + Object { + "action_groups": Array [ + Object { + "id": "default", + "name": "Default", + }, + ], + "action_variables": Object { + "context": Array [], + "state": Array [], + }, + "authorized_consumers": Object {}, + "default_action_group_id": "default", + "enabled_in_license": true, + "id": "1", + "minimum_license_required": "basic", + "name": "name", + "producer": "test", + "recovery_action_group": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + ], + } + `); + + expect(alertsClient.listAlertTypes).toHaveBeenCalledTimes(1); + + expect(res.ok).toHaveBeenCalledWith({ + body: expectedResult, + }); + }); + + it('ensures the license allows listing rule types', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + ruleTypesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule_types"`); + + const listTypes = [ + { + id: '1', + name: 'name', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + authorizedConsumers: {}, + actionVariables: { + context: [], + state: [], + }, + producer: 'alerts', + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, + ]; + + alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents listing rule types', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + ruleTypesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule_types"`); + + const listTypes = [ + { + id: '1', + name: 'name', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + authorizedConsumers: {}, + actionVariables: { + context: [], + state: [], + }, + producer: 'alerts', + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, + ]; + + alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts new file mode 100644 index 0000000000000..a3a44f9b013cd --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { RegistryAlertTypeWithAuth } from '../authorization'; +import { RewriteResponseCase, verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const rewriteBodyRes: RewriteResponseCase = (results) => { + return results.map( + ({ + enabledInLicense, + recoveryActionGroup, + actionGroups, + defaultActionGroupId, + minimumLicenseRequired, + actionVariables, + authorizedConsumers, + ...rest + }) => ({ + ...rest, + 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, + }) + ); +}; + +export const ruleTypesRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ALERTING_API_PATH}/rule_types`, + validate: {}, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const ruleTypes = Array.from(await context.alerting.getAlertsClient().listAlertTypes()); + return res.ok({ + body: rewriteBodyRes(ruleTypes), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/unmute_alert.test.ts b/x-pack/plugins/alerting/server/routes/unmute_alert.test.ts new file mode 100644 index 0000000000000..a491ba394f839 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/unmute_alert.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { unmuteAlertRoute } from './unmute_alert'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('unmuteAlertRoute', () => { + it('unmutes an alert', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unmuteAlertRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot( + `"/api/alerting/rule/{rule_id}/alert/{alert_id}/_unmute"` + ); + + alertsClient.unmuteInstance.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + rule_id: '1', + alert_id: '2', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.unmuteInstance).toHaveBeenCalledTimes(1); + expect(alertsClient.unmuteInstance.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "alertId": "1", + "alertInstanceId": "2", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unmuteAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.unmuteInstance.mockRejectedValue( + new AlertTypeDisabledError('Fail', 'license_invalid') + ); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/unmute_alert.ts b/x-pack/plugins/alerting/server/routes/unmute_alert.ts new file mode 100644 index 0000000000000..94bd6cd9af75f --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/unmute_alert.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { MuteOptions } from '../alerts_client'; +import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + rule_id: schema.string(), + alert_id: schema.string(), +}); + +const rewriteParamsReq: RewriteRequestCase = ({ + rule_id: alertId, + alert_id: alertInstanceId, +}) => ({ + alertId, + alertInstanceId, +}); + +export const unmuteAlertRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{rule_id}/alert/{alert_id}/_unmute`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const params = rewriteParamsReq(req.params); + try { + await alertsClient.unmuteInstance(params); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/unmute_all_rule.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all_rule.test.ts new file mode 100644 index 0000000000000..f873863bcb902 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/unmute_all_rule.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { unmuteAllRuleRoute } from './unmute_all_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('unmuteAllRuleRoute', () => { + it('unmutes a rule', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unmuteAllRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_unmute_all"`); + + alertsClient.unmuteAll.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.unmuteAll).toHaveBeenCalledTimes(1); + expect(alertsClient.unmuteAll.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unmuteAllRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.unmuteAll.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all_rule.ts b/x-pack/plugins/alerting/server/routes/unmute_all_rule.ts new file mode 100644 index 0000000000000..96176e916cd7c --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/unmute_all_rule.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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const unmuteAllRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}/_unmute_all`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + try { + await alertsClient.unmuteAll({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/update_rule.test.ts b/x-pack/plugins/alerting/server/routes/update_rule.test.ts new file mode 100644 index 0000000000000..a7121214cd3d3 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/update_rule.test.ts @@ -0,0 +1,215 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { pick } from 'lodash'; +import { updateRuleRoute } from './update_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { UpdateOptions } from '../alerts_client'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; +import { AlertNotifyWhenType } from '../../common'; +import { AsApiContract } from './lib'; +import { PartialAlert } from '../types'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('updateRuleRoute', () => { + const mockedAlert = { + id: '1', + name: 'abc', + alertTypeId: '1', + tags: ['foo'], + throttle: '10m', + schedule: { interval: '12s' }, + params: { + otherField: false, + }, + createdAt: new Date(), + updatedAt: new Date(), + actions: [ + { + group: 'default', + id: '2', + actionTypeId: 'test', + params: { + baz: true, + }, + }, + ], + notifyWhen: 'onActionGroupChange' as AlertNotifyWhenType, + }; + + const updateRequest: AsApiContract['data']> = { + ...pick(mockedAlert, 'name', 'tags', 'schedule', 'params', 'throttle'), + notify_when: mockedAlert.notifyWhen, + actions: [ + { + group: mockedAlert.actions[0].group, + id: mockedAlert.actions[0].id, + params: mockedAlert.actions[0].params, + }, + ], + }; + + const updateResult: AsApiContract> = { + ...updateRequest, + id: mockedAlert.id, + updated_at: mockedAlert.updatedAt, + created_at: mockedAlert.createdAt, + rule_type_id: mockedAlert.alertTypeId, + actions: mockedAlert.actions.map(({ actionTypeId, ...rest }) => ({ + ...rest, + connector_type_id: actionTypeId, + })), + }; + + it('updates a rule with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateRuleRoute(router, licenseState); + + const [config, handler] = router.put.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}"`); + + alertsClient.update.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + body: updateRequest, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toEqual({ body: updateResult }); + + expect(alertsClient.update).toHaveBeenCalledTimes(1); + expect(alertsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object { + "actions": Array [ + Object { + "group": "default", + "id": "2", + "params": Object { + "baz": true, + }, + }, + ], + "name": "abc", + "notifyWhen": "onActionGroupChange", + "params": Object { + "otherField": false, + }, + "schedule": Object { + "interval": "12s", + }, + "tags": Array [ + "foo", + ], + "throttle": "10m", + }, + "id": "1", + }, + ] + `); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('ensures the license allows updating rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateRuleRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + alertsClient.update.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + body: updateRequest, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents updating rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + updateRuleRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + alertsClient.update.mockResolvedValueOnce(mockedAlert); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + body: updateRequest, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateRuleRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + alertsClient.update.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts new file mode 100644 index 0000000000000..ef5bd00558752 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState, AlertTypeDisabledError, validateDurationSchema } from '../lib'; +import { AlertNotifyWhenType } from '../../common'; +import { UpdateOptions } from '../alerts_client'; +import { + verifyAccessAndContext, + RewriteResponseCase, + RewriteRequestCase, + handleDisabledApiKeysError, +} from './lib'; +import { + AlertTypeParams, + AlertingRequestHandlerContext, + BASE_ALERTING_API_PATH, + validateNotifyWhenType, + PartialAlert, +} from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object({ + name: schema.string(), + tags: schema.arrayOf(schema.string(), { defaultValue: [] }), + schedule: schema.object({ + interval: schema.string({ validate: validateDurationSchema }), + }), + throttle: schema.nullable(schema.string({ validate: validateDurationSchema })), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + actions: schema.arrayOf( + schema.object({ + group: schema.string(), + id: schema.string(), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + { defaultValue: [] } + ), + notify_when: schema.string({ validate: validateNotifyWhenType }), +}); + +const rewriteBodyReq: RewriteRequestCase> = (result) => { + const { notify_when: notifyWhen, ...rest } = result.data; + return { + ...result, + data: { + ...rest, + notifyWhen, + }, + }; +}; +const rewriteBodyRes: RewriteResponseCase> = ({ + actions, + alertTypeId, + scheduledTaskId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus, + ...rest +}) => ({ + ...rest, + api_key_owner: apiKeyOwner, + created_by: createdBy, + updated_by: updatedBy, + ...(alertTypeId ? { rule_type_id: alertTypeId } : {}), + ...(scheduledTaskId ? { scheduled_task_id: scheduledTaskId } : {}), + ...(createdAt ? { created_at: createdAt } : {}), + ...(updatedAt ? { updated_at: updatedAt } : {}), + ...(notifyWhen ? { notify_when: notifyWhen } : {}), + ...(muteAll !== undefined ? { mute_all: muteAll } : {}), + ...(mutedInstanceIds ? { muted_alert_ids: mutedInstanceIds } : {}), + ...(executionStatus + ? { + execution_status: { + status: executionStatus.status, + last_execution_date: executionStatus.lastExecutionDate, + }, + } + : {}), + ...(actions + ? { + actions: actions.map(({ group, id, actionTypeId, params }) => ({ + group, + id, + params, + connector_type_id: actionTypeId, + })), + } + : {}), +}); + +export const updateRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.put( + { + path: `${BASE_ALERTING_API_PATH}/rule/{id}`, + validate: { + body: bodySchema, + params: paramSchema, + }, + }, + handleDisabledApiKeysError( + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + const rule = req.body; + try { + const alertRes = await alertsClient.update( + rewriteBodyReq({ + id, + data: { + ...rule, + notify_when: rule.notify_when as AlertNotifyWhenType, + }, + }) + ); + return res.ok({ + body: rewriteBodyRes(alertRes), + }); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/update_rule_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_rule_api_key.test.ts new file mode 100644 index 0000000000000..ced335136adb1 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/update_rule_api_key.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { updateRuleApiKeyRoute } from './update_rule_api_key'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('updateRuleApiKeyRoute', () => { + it('updates api key for a rule', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateRuleApiKeyRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_update_api_key"`); + + alertsClient.updateApiKey.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(alertsClient.updateApiKey).toHaveBeenCalledTimes(1); + expect(alertsClient.updateApiKey.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateRuleApiKeyRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.updateApiKey.mockRejectedValue( + new AlertTypeDisabledError('Fail', 'license_invalid') + ); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/update_rule_api_key.ts b/x-pack/plugins/alerting/server/routes/update_rule_api_key.ts new file mode 100644 index 0000000000000..57206c68d448d --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/update_rule_api_key.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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, AlertTypeDisabledError } from '../lib'; +import { verifyAccessAndContext } from './lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const updateRuleApiKeyRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_update_api_key`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + try { + await alertsClient.updateApiKey({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index 3c9decdf7ba96..cce394d70ed6f 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -13,6 +13,7 @@ describe('alerts telemetry', () => { test('getTotalCountInUse should replace first "." symbol to "__" in alert types names', async () => { const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; mockEsClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { byAlertTypeId: { diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 93bed31ce7d50..46ac3e53895eb 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -246,50 +246,59 @@ export async function getTotalCountAggregations( }, }); - const totalAlertsCount = Object.keys(results.aggregations.byAlertTypeId.value.types).reduce( + const aggregations = results.aggregations as { + byAlertTypeId: { value: { types: Record } }; + throttleTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; + intervalTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; + connectorsAgg: { + connectors: { + value: { min: number; max: number; totalActionsCount: number; totalAlertsCount: number }; + }; + }; + }; + + const totalAlertsCount = Object.keys(aggregations.byAlertTypeId.value.types).reduce( (total: number, key: string) => - parseInt(results.aggregations.byAlertTypeId.value.types[key], 0) + total, + parseInt(aggregations.byAlertTypeId.value.types[key], 0) + total, 0 ); return { count_total: totalAlertsCount, - count_by_type: Object.keys(results.aggregations.byAlertTypeId.value.types).reduce( + count_by_type: Object.keys(aggregations.byAlertTypeId.value.types).reduce( // ES DSL aggregations are returned as `any` by esClient.search // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, - [replaceFirstAndLastDotSymbols(key)]: results.aggregations.byAlertTypeId.value.types[key], + [replaceFirstAndLastDotSymbols(key)]: aggregations.byAlertTypeId.value.types[key], }), {} ), throttle_time: { - min: `${results.aggregations.throttleTime.value.min}s`, + min: `${aggregations.throttleTime.value.min}s`, avg: `${ - results.aggregations.throttleTime.value.totalCount > 0 - ? results.aggregations.throttleTime.value.totalSum / - results.aggregations.throttleTime.value.totalCount + aggregations.throttleTime.value.totalCount > 0 + ? aggregations.throttleTime.value.totalSum / aggregations.throttleTime.value.totalCount : 0 }s`, - max: `${results.aggregations.throttleTime.value.max}s`, + max: `${aggregations.throttleTime.value.max}s`, }, schedule_time: { - min: `${results.aggregations.intervalTime.value.min}s`, + min: `${aggregations.intervalTime.value.min}s`, avg: `${ - results.aggregations.intervalTime.value.totalCount > 0 - ? results.aggregations.intervalTime.value.totalSum / - results.aggregations.intervalTime.value.totalCount + aggregations.intervalTime.value.totalCount > 0 + ? aggregations.intervalTime.value.totalSum / aggregations.intervalTime.value.totalCount : 0 }s`, - max: `${results.aggregations.intervalTime.value.max}s`, + max: `${aggregations.intervalTime.value.max}s`, }, connectors_per_alert: { - min: results.aggregations.connectorsAgg.connectors.value.min, + min: aggregations.connectorsAgg.connectors.value.min, avg: totalAlertsCount > 0 - ? results.aggregations.connectorsAgg.connectors.value.totalActionsCount / totalAlertsCount + ? aggregations.connectorsAgg.connectors.value.totalActionsCount / totalAlertsCount : 0, - max: results.aggregations.connectorsAgg.connectors.value.max, + max: aggregations.connectorsAgg.connectors.value.max, }, }; } @@ -308,20 +317,23 @@ export async function getTotalCountInUse(esClient: ElasticsearchClient, kibanaIn }, }, }); + + const aggregations = searchResult.aggregations as { + byAlertTypeId: { value: { types: Record } }; + }; + return { - countTotal: Object.keys(searchResult.aggregations.byAlertTypeId.value.types).reduce( + countTotal: Object.keys(aggregations.byAlertTypeId.value.types).reduce( (total: number, key: string) => - parseInt(searchResult.aggregations.byAlertTypeId.value.types[key], 0) + total, + parseInt(aggregations.byAlertTypeId.value.types[key], 0) + total, 0 ), - countByType: Object.keys(searchResult.aggregations.byAlertTypeId.value.types).reduce( + countByType: Object.keys(aggregations.byAlertTypeId.value.types).reduce( // ES DSL aggregations are returned as `any` by esClient.search // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, - [replaceFirstAndLastDotSymbols(key)]: searchResult.aggregations.byAlertTypeId.value.types[ - key - ], + [replaceFirstAndLastDotSymbols(key)]: aggregations.byAlertTypeId.value.types[key], }), {} ), diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts index 884120d3d03df..59aeb4854d9f0 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts @@ -16,6 +16,7 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { // Known alerts (searching the use of the alerts API `registerType`: // Built-in '__index-threshold': { type: 'long' }, + '__es-query': { type: 'long' }, // APM apm__error_rate: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention apm__transaction_error_rate: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention @@ -41,6 +42,10 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { xpack__uptime__alerts__monitorStatus: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention xpack__uptime__alerts__tls: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention xpack__uptime__alerts__durationAnomaly: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention + // Maps + '__geo-containment': { type: 'long' }, + // ML + xpack_ml_anomaly_detection_alert: { type: 'long' }, }; export function createAlertsUsageCollector( diff --git a/x-pack/plugins/apm/common/apm_api/parse_endpoint.ts b/x-pack/plugins/apm/common/apm_api/parse_endpoint.ts new file mode 100644 index 0000000000000..fb7ef6d36ce25 --- /dev/null +++ b/x-pack/plugins/apm/common/apm_api/parse_endpoint.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. + */ + +type Method = 'get' | 'post' | 'put' | 'delete'; + +export function parseEndpoint( + endpoint: string, + pathParams: Record = {} +) { + const [method, rawPathname] = endpoint.split(' '); + + // replace template variables with path params + const pathname = Object.keys(pathParams).reduce((acc, paramName) => { + return acc.replace(`{${paramName}}`, pathParams[paramName]); + }, rawPathname); + + return { method: parseMethod(method), pathname }; +} + +export function parseMethod(method: string) { + const res = method.trim().toLowerCase() as Method; + + if (!['get', 'post', 'put', 'delete'].includes(res)) { + throw new Error('Endpoint was not prefixed with a valid HTTP method'); + } + + return res; +} diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts index 3316c74d52e38..4212e0430ff5f 100644 --- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts +++ b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts @@ -45,10 +45,10 @@ describe('strictKeysRt', () => { { type: t.intersection([ t.type({ query: t.type({ bar: t.string }) }), - t.partial({ query: t.partial({ _debug: t.boolean }) }), + t.partial({ query: t.partial({ _inspect: t.boolean }) }), ]), - passes: [{ query: { bar: '', _debug: true } }], - fails: [{ query: { _debug: true } }], + passes: [{ query: { bar: '', _inspect: true } }], + fails: [{ query: { _inspect: true } }], }, ]; @@ -91,12 +91,12 @@ describe('strictKeysRt', () => { } as Record); const typeB = t.partial({ - query: t.partial({ _debug: jsonRt.pipe(t.boolean) }), + query: t.partial({ _inspect: jsonRt.pipe(t.boolean) }), }); const value = { query: { - _debug: 'true', + _inspect: 'true', filterNames: JSON.stringify(['host', 'agentName']), }, }; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index b785fcc7dab08..7df6ca343426c 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -8,7 +8,7 @@ import { act } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; -import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public'; +import { AppMountParameters, CoreStart } from 'src/core/public'; import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; @@ -72,7 +72,7 @@ describe('renderApp', () => { embeddable, }; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); - createCallApmApi((core.http as unknown) as HttpSetup); + createCallApmApi((core as unknown) as CoreStart); jest .spyOn(window.console, 'warn') diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 8ea4593bb89a7..787b15d0a5675 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -118,7 +118,7 @@ export const renderApp = ( ) => { const { element } = appMountParameters; - createCallApmApi(core.http); + createCallApmApi(core); // Automatically creates static index pattern and stores as saved object createStaticIndexPattern().catch((e) => { diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 64600dd500bd5..bc14bc1531686 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -120,7 +120,7 @@ export const renderApp = ( // render APM feedback link in global help menu setHelpExtension(core); setReadonlyBadge(core); - createCallApmApi(core.http); + createCallApmApi(core); // Automatically creates static index pattern and stores as saved object createStaticIndexPattern().catch((e) => { diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index 29f74b26d310c..fdfed6eb0d685 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -107,7 +107,11 @@ export function ErrorCountAlertTrigger(props: Props) { ]; const chartPreview = ( - + ); return ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 11aab788ec7f4..b4c78b54f329b 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -13,7 +13,6 @@ import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; -import { TimeSeries } from '../../../../typings/timeseries'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; @@ -116,9 +115,9 @@ export function TransactionDurationAlertTrigger(props: Props) { ] ); - const maxY = getMaxY([ - { data: data ?? [] } as TimeSeries<{ x: number; y: number | null }>, - ]); + const latencyChartPreview = data?.latencyChartPreview ?? []; + + const maxY = getMaxY([{ data: latencyChartPreview }]); const formatter = getDurationFormatter(maxY); const yTickFormat = getResponseTimeTickFormatter(formatter); @@ -127,7 +126,7 @@ export function TransactionDurationAlertTrigger(props: Props) { const chartPreview = ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index de30af4a4707f..c6f9c4efd98b6 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -132,7 +132,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { const chartPreview = ( asPercent(d, 1)} threshold={thresholdAsPercent} /> diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx index 6c94b895f6924..db5932a96fb12 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx @@ -35,7 +35,7 @@ export function BreakdownSeries({ ? EUI_CHARTS_THEME_DARK : EUI_CHARTS_THEME_LIGHT; - const { data, status } = useBreakdowns({ + const { breakdowns, status } = useBreakdowns({ field, value, percentileRange, @@ -49,7 +49,7 @@ export function BreakdownSeries({ // so don't user that here return ( <> - {data?.map(({ data: seriesData, name }, sortIndex) => ( + {breakdowns.map(({ data: seriesData, name }, sortIndex) => (
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 5af7f0682db19..e21aaa08c432d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,12 +17,10 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, searchTerm } = urlParams; - const { min: minP, max: maxP } = percentileRange ?? {}; - return useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end && field && value) { return callApmApi({ @@ -47,4 +45,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }, [end, start, uiFilters, field, value, minP, maxP, searchTerm] ); + + return { breakdowns: data?.pageLoadDistBreakdown ?? [], status }; }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index e3e2a979c48d3..d04bcb79a53e1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -38,6 +38,7 @@ export function MainFilters() { [start, end] ); + const rumServiceNames = data?.rumServices ?? []; const { isSmall } = useBreakPoints(); // on mobile we want it to take full width @@ -48,7 +49,7 @@ export function MainFilters() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx index 4b925e914bfa5..f286f963b4fa0 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx @@ -36,7 +36,7 @@ mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ }), })); -const mockCore: () => [any] = () => { +const mockCore: () => any[] = () => { const core = { embeddable: mockEmbeddable, }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts index c40f6ba2b8850..8ae4c9dc0e01d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts @@ -68,7 +68,7 @@ export function useLocalUIFilters({ }); }; - const { data = getInitialData(filterNames), status } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (shouldFetch && urlParams.start && urlParams.end) { return callApmApi({ @@ -96,7 +96,8 @@ export function useLocalUIFilters({ ] ); - const filters = data.map((filter) => ({ + const localUiFilters = data?.localUiFilters ?? getInitialData(filterNames); + const filters = localUiFilters.map((filter) => ({ ...filter, value: values[filter.name] || [], })); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts index 5b448871804eb..f932cec3cacb6 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts @@ -11,9 +11,9 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { FetchOptions } from '../../../../../common/fetch_options'; export function useCallApi() { - const { http } = useApmPluginContext().core; + const { core } = useApmPluginContext(); return useMemo(() => { - return (options: FetchOptions) => callApi(http, options); - }, [http]); + return (options: FetchOptions) => callApi(core, options); + }, [core]); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index d754710dc84fa..ac1846155569a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -6,7 +6,7 @@ */ import cytoscape from 'cytoscape'; -import { HttpSetup } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import React, { ComponentType } from 'react'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; @@ -21,19 +21,21 @@ export default { component: Popover, decorators: [ (Story: ComponentType) => { - const httpMock = ({ - get: async () => ({ - avgCpuUsage: 0.32809666568309237, - avgErrorRate: 0.556068173242986, - avgMemoryUsage: 0.5504868173242986, - transactionStats: { - avgRequestsPerMinute: 164.47222031860858, - avgTransactionDuration: 61634.38905590272, - }, - }), - } as unknown) as HttpSetup; + const coreMock = ({ + http: { + get: async () => ({ + avgCpuUsage: 0.32809666568309237, + avgErrorRate: 0.556068173242986, + avgMemoryUsage: 0.5504868173242986, + transactionStats: { + avgRequestsPerMinute: 164.47222031860858, + avgTransactionDuration: 61634.38905590272, + }, + }), + }, + } as unknown) as CoreStart; - createCallApmApi(httpMock); + createCallApmApi(coreMock); return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index e762f517ce1b5..71355a84d28d4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -33,7 +33,7 @@ interface Props { } export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { - const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( + const { data: serviceNamesData, status: serviceNamesStatus } = useFetcher( (callApmApi) => { return callApmApi({ endpoint: 'GET /api/apm/settings/agent-configuration/services', @@ -43,8 +43,9 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { [], { preservePreviousData: false } ); + const serviceNames = serviceNamesData?.serviceNames ?? []; - const { data: environments = [], status: environmentStatus } = useFetcher( + const { data: environmentsData, status: environmentsStatus } = useFetcher( (callApmApi) => { if (newConfig.service.name) { return callApmApi({ @@ -59,6 +60,8 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { { preservePreviousData: false } ); + const environments = environmentsData?.environments ?? []; + const { status: agentNameStatus } = useFetcher( async (callApmApi) => { const serviceName = newConfig.service.name; @@ -153,11 +156,11 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { 'xpack.apm.agentConfig.servicePage.environment.fieldLabel', { defaultMessage: 'Service environment' } )} - isLoading={environmentStatus === FETCH_STATUS.LOADING} + isLoading={environmentsStatus === FETCH_STATUS.LOADING} options={environmentOptions} value={newConfig.service.environment} disabled={ - !newConfig.service.name || environmentStatus === FETCH_STATUS.LOADING + !newConfig.service.name || environmentsStatus === FETCH_STATUS.LOADING } onChange={(e) => { e.preventDefault(); diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index 4d2754a677bf7..cd5fa5db89a31 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -7,7 +7,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { HttpSetup } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; @@ -23,10 +23,10 @@ storiesOf( module ) .addDecorator((storyFn) => { - const httpMock = {}; + const coreMock = ({} as unknown) as CoreStart; // mock - createCallApmApi((httpMock as unknown) as HttpSetup); + createCallApmApi(coreMock); const contextMock = { core: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 081a3dbc907c5..3e3bc892e6518 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -16,7 +16,7 @@ import { } from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; -type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>['configurations'][0]; interface Props { config: Config; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index bef0dfc22280c..c098be41968dd 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -32,15 +32,19 @@ import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>['configurations'][0]; interface Props { status: FETCH_STATUS; - data: Config[]; + configurations: Config[]; refetch: () => void; } -export function AgentConfigurationList({ status, data, refetch }: Props) { +export function AgentConfigurationList({ + status, + configurations, + refetch, +}: Props) { const { core } = useApmPluginContext(); const canSave = core.application.capabilities.apm.save; const { basePath } = core.http; @@ -113,7 +117,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { return failurePrompt; } - if (status === FETCH_STATUS.SUCCESS && isEmpty(data)) { + if (status === FETCH_STATUS.SUCCESS && isEmpty(configurations)) { return emptyStatePrompt; } @@ -231,7 +235,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { } columns={columns} - items={data} + items={configurations} initialSortField="service.name" initialSortDirection="asc" initialPageSize={20} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 8aa0c35f36717..3225951fd6c70 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -25,8 +25,10 @@ import { useFetcher } from '../../../../hooks/use_fetcher'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; import { AgentConfigurationList } from './List'; +const INITIAL_DATA = { configurations: [] }; + export function AgentConfigurations() { - const { refetch, data = [], status } = useFetcher( + const { refetch, data = INITIAL_DATA, status } = useFetcher( (callApmApi) => callApmApi({ endpoint: 'GET /api/apm/settings/agent-configuration' }), [], @@ -36,7 +38,7 @@ export function AgentConfigurations() { useTrackPageview({ app: 'apm', path: 'agent_configuration' }); useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); - const hasConfigurations = !isEmpty(data); + const hasConfigurations = !isEmpty(data.configurations); return ( <> @@ -72,7 +74,11 @@ export function AgentConfigurations() { - + ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 9722c99990e3f..9d2b4bba22afb 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -24,7 +24,10 @@ import React, { useEffect, useState } from 'react'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { clearCache } from '../../../../services/rest/callApi'; -import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { + APIReturnType, + callApmApi, +} from '../../../../services/rest/createCallApmApi'; const APM_INDEX_LABELS = [ { @@ -84,8 +87,10 @@ async function saveApmIndices({ clearCache(); } +type ApiResponse = APIReturnType<`GET /api/apm/settings/apm-index-settings`>; + // avoid infinite loop by initializing the state outside the component -const INITIAL_STATE = [] as []; +const INITIAL_STATE: ApiResponse = { apmIndexSettings: [] }; export function ApmIndices() { const { core } = useApmPluginContext(); @@ -108,7 +113,7 @@ export function ApmIndices() { useEffect(() => { setApmIndices( - data.reduce( + data.apmIndexSettings.reduce( (acc, { configurationName, savedValue }) => ({ ...acc, [configurationName]: savedValue, @@ -190,7 +195,7 @@ export function ApmIndices() { {APM_INDEX_LABELS.map(({ configurationName, label }) => { - const matchedConfiguration = data.find( + const matchedConfiguration = data.apmIndexSettings.find( ({ configurationName: configName }) => configName === configurationName ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 0dbc8f6235342..77835afef863a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -24,20 +24,12 @@ import { } from '../../../../../utils/testHelpers'; import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; -const data = [ - { - id: '1', - label: 'label 1', - url: 'url 1', - 'service.name': 'opbeans-java', - }, - { - id: '2', - label: 'label 2', - url: 'url 2', - 'transaction.type': 'request', - }, -]; +const data = { + customLinks: [ + { id: '1', label: 'label 1', url: 'url 1', 'service.name': 'opbeans-java' }, + { id: '2', label: 'label 2', url: 'url 2', 'transaction.type': 'request' }, + ], +}; function getMockAPMContext({ canSave }: { canSave: boolean }) { return ({ @@ -69,7 +61,7 @@ describe('CustomLink', () => { describe('empty prompt', () => { beforeAll(() => { jest.spyOn(hooks, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: hooks.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); @@ -290,7 +282,7 @@ describe('CustomLink', () => { describe('invalid license', () => { beforeAll(() => { jest.spyOn(hooks, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: hooks.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 4b4bc2e8feeab..49fa3eab47862 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -35,7 +35,7 @@ export function CustomLinkOverview() { CustomLink | undefined >(); - const { data: customLinks = [], status, refetch } = useFetcher( + const { data, status, refetch } = useFetcher( async (callApmApi) => { if (hasValidLicense) { return callApmApi({ @@ -46,6 +46,8 @@ export function CustomLinkOverview() { [hasValidLicense] ); + const customLinks = data?.customLinks ?? []; + useEffect(() => { if (customLinkSelected) { setIsFlyoutOpen(true); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 6a11f862994e2..bf9062418313a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -21,6 +21,7 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; @@ -33,6 +34,10 @@ interface Props { onCreateJobSuccess: () => void; onCancel: () => void; } + +type ApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/environments'>; +const INITIAL_DATA: ApiResponse = { environments: [] }; + export function AddEnvironments({ currentEnvironments, onCreateJobSuccess, @@ -42,7 +47,7 @@ export function AddEnvironments({ const { anomalyDetectionJobsRefetch } = useAnomalyDetectionJobsContext(); const canCreateJob = !!application.capabilities.ml.canCreateJob; const { toasts } = notifications; - const { data = [], status } = useFetcher( + const { data = INITIAL_DATA, status } = useFetcher( (callApmApi) => callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/environments`, @@ -51,7 +56,7 @@ export function AddEnvironments({ { preservePreviousData: false } ); - const environmentOptions = data.map((env) => ({ + const environmentOptions = data.environments.map((env) => ({ label: getEnvironmentLabel(env), value: env, disabled: currentEnvironments.includes(env), diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap index 7526eaf1aad64..22a12db680334 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap @@ -1722,7 +1722,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` >
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap index 79b4857068a0f..5c431dee43fe6 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap @@ -7,5 +7,5 @@ exports[` App renders properly 1`] = `
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot index 6a2c2ca3abd21..3432e479bff97 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot @@ -1374,7 +1374,7 @@ exports[`Storyshots shareables/Canvas component 1`] = ` >
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"

You are in a dialog. To close this dialog, hit escape.

Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot index 8f37b253c6352..a522684a978cf 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot @@ -23,18 +23,13 @@ exports[`Storyshots shareables/Footer/Settings component 1`] = `
- -
+ + + +
- +
@@ -594,45 +585,36 @@ exports[`extend index management ilm summary extension should return extension w id="phaseExecutionPopover" isOpen={false} key="phaseExecutionPopover" - ownFocus={false} + ownFocus={true} panelPaddingSize="m" > -
-
- - - -
+ Show definition + + +
-
+ diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index ba2ec28bf6bc1..3db0aa43e0e87 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -47,10 +47,10 @@ Array [ exports[`policy table should show empty state when there are not any policies 1`] = `
( + const response = await client.indices.getIndexTemplate( { name: templateName, }, options ); - const { index_templates: templates } = response.body; - return templates?.find((template) => template.name === templateName)?.index_template; + const { index_templates: templates } = response.body as { + index_templates: TemplateFromEs[]; + }; + return templates.find((template) => template.name === templateName)?.index_template; } async function updateIndexTemplate( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx index 6b3982cb50c59..b2647b175b324 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -7,7 +7,10 @@ import React from 'react'; import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; -import { uiSettingsServiceMock } from '../../../../../../../../../../src/core/public/mocks'; +import { + docLinksServiceMock, + uiSettingsServiceMock, +} from '../../../../../../../../../../src/core/public/mocks'; import { MappingsEditorProvider } from '../../../mappings_editor_context'; import { createKibanaReactContext } from '../../../shared_imports'; @@ -80,10 +83,7 @@ const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ }); const defaultProps = { - docLinks: { - DOC_LINK_VERSION: 'master', - ELASTIC_WEBSITE_URL: 'https://jest.elastic.co', - }, + docLinks: docLinksServiceMock.createStartContract(), }; export const WithAppDependencies = (Comp: any) => (props: any) => ( diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 77917db95a116..9573b9cc6436f 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -91,7 +91,7 @@ const getDataStreamsStats = (client: ElasticsearchClient, name = '*') => { }; const getDataStreamsPrivileges = (client: ElasticsearchClient, names: string[]) => { - return client.security.hasPrivileges({ + return client.security.hasPrivileges({ body: { index: [ { @@ -143,6 +143,7 @@ export function registerGetAllRoute({ dataStreams = enhanceDataStreams({ dataStreams, dataStreamsStats, + // @ts-expect-error PrivilegesFromEs incompatible with ApplicationsPrivileges dataStreamsPrivileges, }); @@ -195,6 +196,7 @@ export function registerGetOneRoute({ const enhancedDataStreams = enhanceDataStreams({ dataStreams, dataStreamsStats, + // @ts-expect-error PrivilegesFromEs incompatible with ApplicationsPrivileges dataStreamsPrivileges, }); const body = deserializeDataStream(enhancedDataStreams[0]); diff --git a/x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts new file mode 100644 index 0000000000000..ad4b2963a41bd --- /dev/null +++ b/x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogSourceConfigurationProperties } from '../http_api/log_sources'; + +// NOTE: Type will change, see below. +type ResolvedLogsSourceConfiguration = LogSourceConfigurationProperties; + +// NOTE: This will handle real resolution for https://github.com/elastic/kibana/issues/92650, via the index patterns service, but for now just +// hands back properties from the saved object (and therefore looks pointless...). +export const resolveLogSourceConfiguration = ( + sourceConfiguration: LogSourceConfigurationProperties +): ResolvedLogsSourceConfiguration => { + return sourceConfiguration; +}; diff --git a/x-pack/plugins/infra/common/metrics_sources/index.ts b/x-pack/plugins/infra/common/metrics_sources/index.ts new file mode 100644 index 0000000000000..a697c65e5a0aa --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_sources/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { omit } from 'lodash'; +import { + SourceConfigurationRT, + SourceStatusRuntimeType, +} from '../source_configuration/source_configuration'; +import { DeepPartial } from '../utility_types'; + +/** + * Properties specific to the Metrics Source Configuration. + */ +export const metricsSourceConfigurationPropertiesRT = rt.strict({ + name: SourceConfigurationRT.props.name, + description: SourceConfigurationRT.props.description, + metricAlias: SourceConfigurationRT.props.metricAlias, + inventoryDefaultView: SourceConfigurationRT.props.inventoryDefaultView, + metricsExplorerDefaultView: SourceConfigurationRT.props.metricsExplorerDefaultView, + fields: rt.strict(omit(SourceConfigurationRT.props.fields.props, 'message')), + anomalyThreshold: rt.number, +}); + +export type MetricsSourceConfigurationProperties = rt.TypeOf< + typeof metricsSourceConfigurationPropertiesRT +>; + +export const partialMetricsSourceConfigurationPropertiesRT = rt.partial({ + ...metricsSourceConfigurationPropertiesRT.type.props, + fields: rt.partial({ + ...metricsSourceConfigurationPropertiesRT.type.props.fields.type.props, + }), +}); + +export type PartialMetricsSourceConfigurationProperties = rt.TypeOf< + typeof partialMetricsSourceConfigurationPropertiesRT +>; + +const metricsSourceConfigurationOriginRT = rt.keyof({ + fallback: null, + internal: null, + stored: null, +}); + +export const metricsSourceStatusRT = rt.strict({ + metricIndicesExist: SourceStatusRuntimeType.props.metricIndicesExist, + indexFields: SourceStatusRuntimeType.props.indexFields, +}); + +export type MetricsSourceStatus = rt.TypeOf; + +export const metricsSourceConfigurationRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + origin: metricsSourceConfigurationOriginRT, + configuration: metricsSourceConfigurationPropertiesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + status: metricsSourceStatusRT, + }), + ]) +); + +export type MetricsSourceConfiguration = rt.TypeOf; +export type PartialMetricsSourceConfiguration = DeepPartial; + +export const metricsSourceConfigurationResponseRT = rt.type({ + source: metricsSourceConfigurationRT, +}); + +export type MetricsSourceConfigurationResponse = rt.TypeOf< + typeof metricsSourceConfigurationResponseRT +>; diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts similarity index 61% rename from x-pack/plugins/infra/common/http_api/source_api.ts rename to x-pack/plugins/infra/common/source_configuration/source_configuration.ts index f14151531ba35..ad68a7a019848 100644 --- a/x-pack/plugins/infra/common/http_api/source_api.ts +++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts @@ -5,8 +5,19 @@ * 2.0. */ +/** + * These are the core source configuration types that represent a Source Configuration in + * it's entirety. There are then subsets of this configuration that form the Logs Source Configuration + * and Metrics Source Configuration. The Logs Source Configuration is further expanded to it's resolved form. + * -> Source Configuration + * -> Logs source configuration + * -> Resolved Logs Source Configuration + * -> Metrics Source Configuration + */ + /* eslint-disable @typescript-eslint/no-empty-interface */ +import { omit } from 'lodash'; import * as rt from 'io-ts'; import moment from 'moment'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -29,121 +40,113 @@ export const TimestampFromString = new rt.Type( ); /** - * Stored source configuration as read from and written to saved objects + * Log columns */ -const SavedSourceConfigurationFieldsRuntimeType = rt.partial({ - container: rt.string, - host: rt.string, - pod: rt.string, - tiebreaker: rt.string, - timestamp: rt.string, -}); - -export type InfraSavedSourceConfigurationFields = rt.TypeOf< - typeof SavedSourceConfigurationFieldColumnRuntimeType ->; - -export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({ +export const SourceConfigurationTimestampColumnRuntimeType = rt.type({ timestampColumn: rt.type({ id: rt.string, }), }); export type InfraSourceConfigurationTimestampColumn = rt.TypeOf< - typeof SavedSourceConfigurationTimestampColumnRuntimeType + typeof SourceConfigurationTimestampColumnRuntimeType >; -export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({ +export const SourceConfigurationMessageColumnRuntimeType = rt.type({ messageColumn: rt.type({ id: rt.string, }), }); export type InfraSourceConfigurationMessageColumn = rt.TypeOf< - typeof SavedSourceConfigurationMessageColumnRuntimeType + typeof SourceConfigurationMessageColumnRuntimeType >; -export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({ +export const SourceConfigurationFieldColumnRuntimeType = rt.type({ fieldColumn: rt.type({ id: rt.string, field: rt.string, }), }); -export const SavedSourceConfigurationColumnRuntimeType = rt.union([ - SavedSourceConfigurationTimestampColumnRuntimeType, - SavedSourceConfigurationMessageColumnRuntimeType, - SavedSourceConfigurationFieldColumnRuntimeType, +export type InfraSourceConfigurationFieldColumn = rt.TypeOf< + typeof SourceConfigurationFieldColumnRuntimeType +>; + +export const SourceConfigurationColumnRuntimeType = rt.union([ + SourceConfigurationTimestampColumnRuntimeType, + SourceConfigurationMessageColumnRuntimeType, + SourceConfigurationFieldColumnRuntimeType, ]); -export type InfraSavedSourceConfigurationColumn = rt.TypeOf< - typeof SavedSourceConfigurationColumnRuntimeType ->; +export type InfraSourceConfigurationColumn = rt.TypeOf; -export const SavedSourceConfigurationRuntimeType = rt.partial({ +/** + * Fields + */ + +const SourceConfigurationFieldsRT = rt.type({ + container: rt.string, + host: rt.string, + pod: rt.string, + tiebreaker: rt.string, + timestamp: rt.string, + message: rt.array(rt.string), +}); + +/** + * Properties that represent a full source configuration, which is the result of merging static values with + * saved values. + */ +export const SourceConfigurationRT = rt.type({ name: rt.string, description: rt.string, metricAlias: rt.string, logAlias: rt.string, inventoryDefaultView: rt.string, metricsExplorerDefaultView: rt.string, - fields: SavedSourceConfigurationFieldsRuntimeType, - logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + fields: SourceConfigurationFieldsRT, + logColumns: rt.array(SourceConfigurationColumnRuntimeType), anomalyThreshold: rt.number, }); +/** + * Stored source configuration as read from and written to saved objects + */ +const SavedSourceConfigurationFieldsRuntimeType = rt.partial( + omit(SourceConfigurationFieldsRT.props, ['message']) +); + +export type InfraSavedSourceConfigurationFields = rt.TypeOf< + typeof SavedSourceConfigurationFieldsRuntimeType +>; + +export const SavedSourceConfigurationRuntimeType = rt.intersection([ + rt.partial(omit(SourceConfigurationRT.props, ['fields'])), + rt.partial({ + fields: SavedSourceConfigurationFieldsRuntimeType, + }), +]); + export interface InfraSavedSourceConfiguration extends rt.TypeOf {} export const pickSavedSourceConfiguration = ( value: InfraSourceConfiguration ): InfraSavedSourceConfiguration => { - const { - name, - description, - metricAlias, - logAlias, - fields, - inventoryDefaultView, - metricsExplorerDefaultView, - logColumns, - anomalyThreshold, - } = value; - const { container, host, pod, tiebreaker, timestamp } = fields; - - return { - name, - description, - metricAlias, - logAlias, - inventoryDefaultView, - metricsExplorerDefaultView, - fields: { container, host, pod, tiebreaker, timestamp }, - logColumns, - anomalyThreshold, - }; + return value; }; /** - * Static source configuration as read from the configuration file + * Static source configuration, the result of merging values from the config file and + * hardcoded defaults. */ -const StaticSourceConfigurationFieldsRuntimeType = rt.partial({ - ...SavedSourceConfigurationFieldsRuntimeType.props, - message: rt.array(rt.string), -}); - +const StaticSourceConfigurationFieldsRuntimeType = rt.partial(SourceConfigurationFieldsRT.props); export const StaticSourceConfigurationRuntimeType = rt.partial({ - name: rt.string, - description: rt.string, - metricAlias: rt.string, - logAlias: rt.string, - inventoryDefaultView: rt.string, - metricsExplorerDefaultView: rt.string, + ...SourceConfigurationRT.props, fields: StaticSourceConfigurationFieldsRuntimeType, - logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), - anomalyThreshold: rt.number, }); export interface InfraStaticSourceConfiguration @@ -153,18 +156,20 @@ export interface InfraStaticSourceConfiguration * Full source configuration type after all cleanup has been done at the edges */ -const SourceConfigurationFieldsRuntimeType = rt.type({ - ...StaticSourceConfigurationFieldsRuntimeType.props, -}); - -export type InfraSourceConfigurationFields = rt.TypeOf; +export type InfraSourceConfigurationFields = rt.TypeOf; export const SourceConfigurationRuntimeType = rt.type({ - ...SavedSourceConfigurationRuntimeType.props, - fields: SourceConfigurationFieldsRuntimeType, - logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + ...SourceConfigurationRT.props, + fields: SourceConfigurationFieldsRT, + logColumns: rt.array(SourceConfigurationColumnRuntimeType), }); +export interface InfraSourceConfiguration + extends rt.TypeOf {} + +/** + * Source status + */ const SourceStatusFieldRuntimeType = rt.type({ name: rt.string, type: rt.string, @@ -175,12 +180,17 @@ const SourceStatusFieldRuntimeType = rt.type({ export type InfraSourceIndexField = rt.TypeOf; -const SourceStatusRuntimeType = rt.type({ +export const SourceStatusRuntimeType = rt.type({ logIndicesExist: rt.boolean, metricIndicesExist: rt.boolean, indexFields: rt.array(SourceStatusFieldRuntimeType), }); +export interface InfraSourceStatus extends rt.TypeOf {} + +/** + * Source configuration along with source status and metadata + */ export const SourceRuntimeType = rt.intersection([ rt.type({ id: rt.string, @@ -198,11 +208,6 @@ export const SourceRuntimeType = rt.intersection([ }), ]); -export interface InfraSourceStatus extends rt.TypeOf {} - -export interface InfraSourceConfiguration - extends rt.TypeOf {} - export interface InfraSource extends rt.TypeOf {} export const SourceResponseRuntimeType = rt.type({ diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx index 88d72300c2d6d..b345e138accec 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx @@ -17,7 +17,7 @@ import { act } from 'react-dom/test-utils'; import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index b28c76d1cb374..c4f8b5a615b0f 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -43,7 +43,7 @@ import { AlertTypeParamsExpressionProps, } from '../../../../../triggers_actions_ui/public'; import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http'; import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items'; import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items'; @@ -124,14 +124,13 @@ export const Expressions: React.FC = (props) => { } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', fetch: http.fetch, toastWarning: notifications.toasts.addWarning, }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx index dd4cbe10b74ee..6b99aff9f903d 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { Expression, AlertContextMeta } from './expression'; import { act } from 'react-dom/test-utils'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx index 12cc2bf9fb3a9..afbd6ffa8b5f7 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -27,7 +27,7 @@ import { AlertTypeParamsExpressionProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/types'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http'; import { findInventoryModel } from '../../../../common/inventory_models'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { NodeTypeExpression } from './node_type'; @@ -75,12 +75,11 @@ export const Expression: React.FC = (props) => { } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', fetch: http.fetch, toastWarning: notifications.toasts.addWarning, }); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx index a6d74d4f461a6..667f5c061ce48 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx @@ -15,7 +15,7 @@ import { act } from 'react-dom/test-utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 3b8afc173c2bd..8835a7cd55ce8 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -35,7 +35,7 @@ import { import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http'; import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; @@ -73,14 +73,13 @@ export const Expressions: React.FC = (props) => { const { http, notifications } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', fetch: http.fetch, toastWarning: notifications.toasts.addWarning, }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index 7e4209e4253d7..caf8e32814fe5 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { coreMock as mockCoreMock } from 'src/core/public/mocks'; import { MetricExpression } from '../types'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import React from 'react'; import { ExpressionChart } from './expression_chart'; import { act } from 'react-dom/test-utils'; @@ -45,20 +45,17 @@ describe('ExpressionChart', () => { fields: [], }; - const source: InfraSource = { + const source: MetricsSourceConfiguration = { id: 'default', origin: 'fallback', configuration: { name: 'default', description: 'The default configuration', - logColumns: [], metricAlias: 'metricbeat-*', - logAlias: 'filebeat-*', inventoryDefaultView: 'host', metricsExplorerDefaultView: 'host', fields: { timestamp: '@timestamp', - message: ['message'], container: 'container.id', host: 'host.name', pod: 'kubernetes.pod.uid', diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 2a274c4b6d50f..e5558b961ab20 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -11,7 +11,7 @@ import { first, last } from 'lodash'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { Color } from '../../../../common/color_palette'; import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; @@ -35,7 +35,7 @@ import { ThresholdAnnotations } from '../../common/criterion_preview_chart/thres interface Props { expression: MetricExpression; derivedIndexPattern: IIndexPattern; - source: InfraSource | null; + source: MetricsSourceConfiguration | null; filterQuery?: string; groupBy?: string | string[]; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx index 54477a39c2626..90f75e6a94022 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx @@ -13,7 +13,7 @@ import { act } from 'react-dom/test-utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 908372d13b6bc..e3006993216ae 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -7,7 +7,7 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { useMemo } from 'react'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { MetricExpression } from '../types'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; @@ -15,7 +15,7 @@ import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/ export const useMetricsExplorerChartData = ( expression: MetricExpression, derivedIndexPattern: IIndexPattern, - source: InfraSource | null, + source: MetricsSourceConfiguration | null, filterQuery?: string, groupBy?: string | string[] ) => { diff --git a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx deleted file mode 100644 index b5b28cb25b83b..0000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx +++ /dev/null @@ -1,156 +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, { useCallback, useMemo, useState } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - LogColumnConfiguration, - isTimestampLogColumnConfiguration, - isMessageLogColumnConfiguration, - TimestampLogColumnConfiguration, - MessageLogColumnConfiguration, - FieldLogColumnConfiguration, -} from '../../utils/source_configuration'; - -export interface TimestampLogColumnConfigurationProps { - logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn']; - remove: () => void; - type: 'timestamp'; -} - -export interface MessageLogColumnConfigurationProps { - logColumnConfiguration: MessageLogColumnConfiguration['messageColumn']; - remove: () => void; - type: 'message'; -} - -export interface FieldLogColumnConfigurationProps { - logColumnConfiguration: FieldLogColumnConfiguration['fieldColumn']; - remove: () => void; - type: 'field'; -} - -export type LogColumnConfigurationProps = - | TimestampLogColumnConfigurationProps - | MessageLogColumnConfigurationProps - | FieldLogColumnConfigurationProps; - -interface FormState { - logColumns: LogColumnConfiguration[]; -} - -type FormStateChanges = Partial; - -export const useLogColumnsConfigurationFormState = ({ - initialFormState = defaultFormState, -}: { - initialFormState?: FormState; -}) => { - const [formStateChanges, setFormStateChanges] = useState({}); - - const resetForm = useCallback(() => setFormStateChanges({}), []); - - const formState = useMemo( - () => ({ - ...initialFormState, - ...formStateChanges, - }), - [initialFormState, formStateChanges] - ); - - const logColumnConfigurationProps = useMemo( - () => - formState.logColumns.map( - (logColumn): LogColumnConfigurationProps => { - const remove = () => - setFormStateChanges((changes) => ({ - ...changes, - logColumns: formState.logColumns.filter((item) => item !== logColumn), - })); - - if (isTimestampLogColumnConfiguration(logColumn)) { - return { - logColumnConfiguration: logColumn.timestampColumn, - remove, - type: 'timestamp', - }; - } else if (isMessageLogColumnConfiguration(logColumn)) { - return { - logColumnConfiguration: logColumn.messageColumn, - remove, - type: 'message', - }; - } else { - return { - logColumnConfiguration: logColumn.fieldColumn, - remove, - type: 'field', - }; - } - } - ), - [formState.logColumns] - ); - - const addLogColumn = useCallback( - (logColumnConfiguration: LogColumnConfiguration) => - setFormStateChanges((changes) => ({ - ...changes, - logColumns: [...formState.logColumns, logColumnConfiguration], - })), - [formState.logColumns] - ); - - const moveLogColumn = useCallback( - (sourceIndex, destinationIndex) => { - if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) { - const newLogColumns = [...formState.logColumns]; - newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); - setFormStateChanges((changes) => ({ - ...changes, - logColumns: newLogColumns, - })); - } - }, - [formState.logColumns] - ); - - const errors = useMemo( - () => - logColumnConfigurationProps.length <= 0 - ? [ - , - ] - : [], - [logColumnConfigurationProps] - ); - - const isFormValid = useMemo(() => (errors.length <= 0 ? true : false), [errors]); - - const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); - - return { - addLogColumn, - moveLogColumn, - errors, - logColumnConfigurationProps, - formState, - formStateChanges, - isFormDirty, - isFormValid, - resetForm, - }; -}; - -const defaultFormState: FormState = { - logColumns: [], -}; diff --git a/x-pack/plugins/infra/public/containers/source/index.ts b/x-pack/plugins/infra/public/containers/metrics_source/index.ts similarity index 100% rename from x-pack/plugins/infra/public/containers/source/index.ts rename to x-pack/plugins/infra/public/containers/metrics_source/index.ts diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/metrics_source/source.tsx similarity index 79% rename from x-pack/plugins/infra/public/containers/source/source.tsx rename to x-pack/plugins/infra/public/containers/metrics_source/source.tsx index 8e2a8f29e03df..b730f8b007e43 100644 --- a/x-pack/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/plugins/infra/public/containers/metrics_source/source.tsx @@ -9,27 +9,25 @@ import createContainer from 'constate'; import { useEffect, useMemo, useState } from 'react'; import { - InfraSavedSourceConfiguration, - InfraSource, - SourceResponse, -} from '../../../common/http_api/source_api'; + MetricsSourceConfigurationResponse, + MetricsSourceConfiguration, + PartialMetricsSourceConfigurationProperties, +} from '../../../common/metrics_sources'; + import { useTrackedPromise } from '../../utils/use_tracked_promise'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; export const pickIndexPattern = ( - source: InfraSource | undefined, - type: 'logs' | 'metrics' | 'both' + source: MetricsSourceConfiguration | undefined, + type: 'metrics' ) => { if (!source) { return 'unknown-index'; } - if (type === 'logs') { - return source.configuration.logAlias; - } if (type === 'metrics') { return source.configuration.metricAlias; } - return `${source.configuration.logAlias},${source.configuration.metricAlias}`; + return `${source.configuration.metricAlias}`; }; const DEPENDENCY_ERROR_MESSAGE = 'Failed to load source: No fetch client available.'; @@ -39,7 +37,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const fetchService = kibana.services.http?.fetch; const API_URL = `/api/metrics/source/${sourceId}`; - const [source, setSource] = useState(undefined); + const [source, setSource] = useState(undefined); const [loadSourceRequest, loadSource] = useTrackedPromise( { @@ -49,7 +47,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { throw new Error(DEPENDENCY_ERROR_MESSAGE); } - return await fetchService(`${API_URL}/metrics`, { + return await fetchService(`${API_URL}`, { method: 'GET', }); }, @@ -62,12 +60,12 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const [createSourceConfigurationRequest, createSourceConfiguration] = useTrackedPromise( { - createPromise: async (sourceProperties: InfraSavedSourceConfiguration) => { + createPromise: async (sourceProperties: PartialMetricsSourceConfigurationProperties) => { if (!fetchService) { throw new Error(DEPENDENCY_ERROR_MESSAGE); } - return await fetchService(API_URL, { + return await fetchService(API_URL, { method: 'PATCH', body: JSON.stringify(sourceProperties), }); @@ -83,12 +81,12 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( { - createPromise: async (sourceProperties: InfraSavedSourceConfiguration) => { + createPromise: async (sourceProperties: PartialMetricsSourceConfigurationProperties) => { if (!fetchService) { throw new Error(DEPENDENCY_ERROR_MESSAGE); } - return await fetchService(API_URL, { + return await fetchService(API_URL, { method: 'PATCH', body: JSON.stringify(sourceProperties), }); @@ -102,7 +100,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { [fetchService, sourceId] ); - const createDerivedIndexPattern = (type: 'logs' | 'metrics' | 'both') => { + const createDerivedIndexPattern = (type: 'metrics') => { return { fields: source?.status ? source.status.indexFields : [], title: pickIndexPattern(source, type), @@ -129,9 +127,6 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const sourceExists = useMemo(() => (source ? !!source.version : undefined), [source]); - const logIndicesExist = useMemo(() => source && source.status && source.status.logIndicesExist, [ - source, - ]); const metricIndicesExist = useMemo( () => source && source.status && source.status.metricIndicesExist, [source] @@ -144,7 +139,6 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { return { createSourceConfiguration, createDerivedIndexPattern, - logIndicesExist, isLoading, isLoadingSource: loadSourceRequest.state === 'pending', isUninitialized, diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/metrics_source/use_source_via_http.ts similarity index 62% rename from x-pack/plugins/infra/public/containers/source/use_source_via_http.ts rename to x-pack/plugins/infra/public/containers/metrics_source/use_source_via_http.ts index 548e6b8aa9cd9..2947f8fb09847 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/metrics_source/use_source_via_http.ts @@ -13,51 +13,47 @@ import createContainer from 'constate'; import { HttpHandler } from 'src/core/public'; import { ToastInput } from 'src/core/public'; import { - SourceResponseRuntimeType, - SourceResponse, - InfraSource, -} from '../../../common/http_api/source_api'; + metricsSourceConfigurationResponseRT, + MetricsSourceConfigurationResponse, + MetricsSourceConfiguration, +} from '../../../common/metrics_sources'; import { useHTTPRequest } from '../../hooks/use_http_request'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; export const pickIndexPattern = ( - source: InfraSource | undefined, - type: 'logs' | 'metrics' | 'both' + source: MetricsSourceConfiguration | undefined, + type: 'metrics' ) => { if (!source) { return 'unknown-index'; } - if (type === 'logs') { - return source.configuration.logAlias; - } if (type === 'metrics') { return source.configuration.metricAlias; } - return `${source.configuration.logAlias},${source.configuration.metricAlias}`; + return `${source.configuration.metricAlias}`; }; interface Props { sourceId: string; - type: 'logs' | 'metrics' | 'both'; fetch?: HttpHandler; toastWarning?: (input: ToastInput) => void; } -export const useSourceViaHttp = ({ - sourceId = 'default', - type = 'both', - fetch, - toastWarning, -}: Props) => { +export const useSourceViaHttp = ({ sourceId = 'default', fetch, toastWarning }: Props) => { const decodeResponse = (response: any) => { return pipe( - SourceResponseRuntimeType.decode(response), + metricsSourceConfigurationResponseRT.decode(response), fold(throwErrors(createPlainError), identity) ); }; - const { error, loading, response, makeRequest } = useHTTPRequest( - `/api/metrics/source/${sourceId}/${type}`, + const { + error, + loading, + response, + makeRequest, + } = useHTTPRequest( + `/api/metrics/source/${sourceId}`, 'GET', null, decodeResponse, @@ -71,15 +67,12 @@ export const useSourceViaHttp = ({ })(); }, [makeRequest]); - const createDerivedIndexPattern = useCallback( - (indexType: 'logs' | 'metrics' | 'both' = type) => { - return { - fields: response?.source.status ? response.source.status.indexFields : [], - title: pickIndexPattern(response?.source, indexType), - }; - }, - [response, type] - ); + const createDerivedIndexPattern = useCallback(() => { + return { + fields: response?.source.status ? response.source.status.indexFields : [], + title: pickIndexPattern(response?.source, 'metrics'), + }; + }, [response]); const source = useMemo(() => { return response ? response.source : null; diff --git a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx index 4c4835cbe4cdb..56a2a13e31ff7 100644 --- a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx +++ b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx @@ -17,10 +17,10 @@ import { useUrlState } from '../../utils/use_url_state'; import { useFindSavedObject } from '../../hooks/use_find_saved_object'; import { useCreateSavedObject } from '../../hooks/use_create_saved_object'; import { useDeleteSavedObject } from '../../hooks/use_delete_saved_object'; -import { Source } from '../source'; +import { Source } from '../metrics_source'; import { metricsExplorerViewSavedObjectName } from '../../../common/saved_objects/metrics_explorer_view'; import { inventoryViewSavedObjectName } from '../../../common/saved_objects/inventory_view'; -import { useSourceConfigurationFormState } from '../../components/source_configuration/source_configuration_form_state'; +import { useSourceConfigurationFormState } from '../../pages/metrics/settings/source_configuration_form_state'; import { useGetSavedObject } from '../../hooks/use_get_saved_object'; import { useUpdateSavedObject } from '../../hooks/use_update_saved_object'; diff --git a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx index 3b9f0d3e1eae2..f3ca57a40c4c7 100644 --- a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx +++ b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx @@ -9,17 +9,19 @@ import React, { useContext } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; import { - InfraSavedSourceConfiguration, - InfraSourceConfiguration, -} from '../../../common/http_api/source_api'; + MetricsSourceConfigurationProperties, + PartialMetricsSourceConfigurationProperties, +} from '../../../common/metrics_sources'; import { RendererFunction } from '../../utils/typed_react'; -import { Source } from '../source'; +import { Source } from '../metrics_source'; interface WithSourceProps { children: RendererFunction<{ - configuration?: InfraSourceConfiguration; - create: (sourceProperties: InfraSavedSourceConfiguration) => Promise | undefined; - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + configuration?: MetricsSourceConfigurationProperties; + create: ( + sourceProperties: PartialMetricsSourceConfigurationProperties + ) => Promise | undefined; + createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; exists?: boolean; hasFailed: boolean; isLoading: boolean; @@ -29,7 +31,9 @@ interface WithSourceProps { metricAlias?: string; metricIndicesExist?: boolean; sourceId: string; - update: (sourceProperties: InfraSavedSourceConfiguration) => Promise | undefined; + update: ( + sourceProperties: PartialMetricsSourceConfigurationProperties + ) => Promise | undefined; version?: string; }>; } @@ -42,7 +46,6 @@ export const WithSource: React.FunctionComponent = ({ children sourceExists, sourceId, metricIndicesExist, - logIndicesExist, isLoading, loadSource, hasFailedLoadingSource, @@ -60,7 +63,6 @@ export const WithSource: React.FunctionComponent = ({ children isLoading, lastFailureMessage: loadSourceFailureMessage, load: loadSource, - logIndicesExist, metricIndicesExist, sourceId, update: updateSourceConfiguration, diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 622e0c9d33845..4541eb6518788 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -14,7 +14,7 @@ import { SnapshotNodeMetric, SnapshotNodePath, } from '../../common/http_api/snapshot_api'; -import { InfraSourceConfigurationFields } from '../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../common/metrics_sources'; import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options'; export interface InfraWaffleMapNode { @@ -124,7 +124,7 @@ export enum InfraWaffleMapRuleOperator { } export interface InfraWaffleMapOptions { - fields?: InfraSourceConfigurationFields | null; + fields?: MetricsSourceConfigurationProperties['fields'] | null; formatter: InfraFormatterType; formatTemplate: string; metric: SnapshotMetricInput; diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts index 45b17aeb1f724..bcc2eec504209 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts @@ -14,9 +14,7 @@ export const createMetricsHasData = ( ) => async () => { const [coreServices] = await getStartServices(); const { http } = coreServices; - const results = await http.get<{ hasData: boolean }>( - '/api/metrics/source/default/metrics/hasData' - ); + const results = await http.get<{ hasData: boolean }>('/api/metrics/source/default/hasData'); return results.hasData; }; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index ea2e67abc4141..8377eadfbce1d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -14,7 +14,7 @@ import { useHostIpToName } from './use_host_ip_to_name'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { LoadingPage } from '../../components/loading_page'; import { Error } from '../error'; -import { useSource } from '../../containers/source/source'; +import { useSourceViaHttp } from '../../containers/metrics_source/use_source_via_http'; type RedirectToHostDetailType = RouteComponentProps<{ hostIp: string; @@ -26,7 +26,7 @@ export const RedirectToHostDetailViaIP = ({ }, location, }: RedirectToHostDetailType) => { - const { source } = useSource({ sourceId: 'default' }); + const { source } = useSourceViaHttp({ sourceId: 'default' }); const { error, name } = useHostIpToName( hostIp, diff --git a/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx index 13eea67fb2a5a..236817ce3890f 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InputFieldProps } from '../../../components/source_configuration'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface FieldsConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx index 72b5c35b958d6..e6f03e76255a2 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InputFieldProps } from '../../../components/source_configuration'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface IndicesConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index 5d6ff9544e187..bc3bc22f3f1b2 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { NoIndices } from '../../../components/empty_states/no_indices'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration/view_source_configuration_button'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useLinkProps } from '../../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 240cb778275b1..51cc4ca098483 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -12,7 +12,7 @@ import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/common'; -import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; @@ -24,7 +24,7 @@ import { } from './metrics_explorer/hooks/use_metrics_explorer_options'; import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; import { WithSource } from '../../containers/with_source'; -import { Source } from '../../containers/source'; +import { Source } from '../../containers/metrics_source'; import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './inventory_view'; import { MetricsSettingsPage } from './settings'; @@ -188,8 +188,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { }; const PageContent = (props: { - configuration: InfraSourceConfiguration; - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + configuration: MetricsSourceConfigurationProperties; + createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; }) => { const { createDerivedIndexPattern, configuration } = props; const { options } = useContext(MetricsExplorerOptionsContainer.Context); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 089ad9c237818..534132eb75fa1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -18,7 +18,7 @@ import { useSnapshot } from '../hooks/use_snaphot'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; -import { useSourceContext } from '../../../../containers/source'; +import { useSourceContext } from '../../../../containers/metrics_source'; import { InfraFormatterType } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx index 7f0424cf48758..409c11cbbe897 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx @@ -43,7 +43,7 @@ import { import { PaginationControls } from './pagination'; import { AnomalySummary } from './annomaly_summary'; import { AnomalySeverityIndicator } from '../../../../../../../components/logging/log_analysis_results/anomaly_severity_indicator'; -import { useSourceContext } from '../../../../../../../containers/source'; +import { useSourceContext } from '../../../../../../../containers/metrics_source'; import { createResultsUrl } from '../flyout_home'; import { useWaffleViewState, WaffleViewState } from '../../../../hooks/use_waffle_view_state'; type JobType = 'k8s' | 'hosts'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx index 326689e945e1d..387e739fab43f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx @@ -13,7 +13,7 @@ import { JobSetupScreen } from './job_setup_screen'; import { useInfraMLCapabilities } from '../../../../../../containers/ml/infra_ml_capabilities'; import { MetricHostsModuleProvider } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { MetricK8sModuleProvider } from '../../../../../../containers/ml/modules/metrics_k8s/module'; -import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../../../../containers/metrics_source/use_source_via_http'; import { useActiveKibanaSpace } from '../../../../../../hooks/use_kibana_space'; export const AnomalyDetectionFlyout = () => { @@ -23,7 +23,6 @@ export const AnomalyDetectionFlyout = () => { const [screenParams, setScreenParams] = useState(null); const { source } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', }); const { space } = useActiveKibanaSpace(); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index 894f76318bcfe..a210831eef865 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -17,7 +17,7 @@ import moment, { Moment } from 'moment'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../../../../containers/metrics_source/use_source_via_http'; import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module'; import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; @@ -42,7 +42,6 @@ export const JobSetupScreen = (props: Props) => { const [filterQuery, setFilterQuery] = useState(''); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', }); const indicies = h.sourceConfiguration.indices; @@ -79,7 +78,7 @@ export const JobSetupScreen = (props: Props) => { } }, [props.jobType, k.jobSummaries, h.jobSummaries]); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx index d89aaefe53fd1..5ab8eb380a657 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx @@ -23,7 +23,7 @@ import { EuiLoadingChart, EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/e import { TabContent, TabProps } from '../shared'; import { useSnapshot } from '../../../../hooks/use_snaphot'; import { useWaffleOptionsContext } from '../../../../hooks/use_waffle_options'; -import { useSourceContext } from '../../../../../../../containers/source'; +import { useSourceContext } from '../../../../../../../containers/metrics_source'; import { findInventoryFields } from '../../../../../../../../common/inventory_models'; import { convertKueryToElasticSearchQuery } from '../../../../../../../utils/kuery'; import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx index 9aa2cdfd90203..010a1a9941335 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLoadingChart } from '@elastic/eui'; import { TabContent, TabProps } from '../shared'; -import { Source } from '../../../../../../../containers/source'; +import { Source } from '../../../../../../../containers/metrics_source'; import { findInventoryModel } from '../../../../../../../../common/inventory_models'; import { InventoryItemType } from '../../../../../../../../common/inventory_models/types'; import { useMetadata } from '../../../../../metric_detail/hooks/use_metadata'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx index cae17c174772d..16f73734836d0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { Source } from '../../../../containers/source'; +import { Source } from '../../../../containers/metrics_source'; import { AutocompleteField } from '../../../../components/autocomplete_field'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index 0248241d616dc..0a657b5242427 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -29,7 +29,7 @@ import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_reac import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n'; import { MetricsExplorerAggregation } from '../../../../../../common/http_api'; import { colorTransformer, Color } from '../../../../../../common/color_palette'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { useTimeline } from '../../hooks/use_timeline'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx index cd05341156831..1c79807f139c3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -7,7 +7,7 @@ import React, { FunctionComponent } from 'react'; import { EuiFlexItem } from '@elastic/eui'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { SnapshotMetricInput, SnapshotGroupBy, @@ -24,7 +24,7 @@ import { WaffleOptionsState, WaffleSortOption } from '../../hooks/use_waffle_opt import { useInventoryMeta } from '../../hooks/use_inventory_meta'; export interface ToolbarProps extends Omit { - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; changeMetric: (payload: SnapshotMetricInput) => void; changeGroupBy: (payload: SnapshotGroupBy) => void; changeCustomOptions: (payload: InfraGroupByOptions[]) => void; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index abc0089e4fc2e..7fc332ead45c7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { fieldToName } from '../../lib/field_to_display_name'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher'; import { ToolbarProps } from './toolbar'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx index 523fa5f013b5a..6dde53efae761 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -17,7 +17,7 @@ import { InfraFormatterType, } from '../../../../../lib/lib'; -jest.mock('../../../../../containers/source', () => ({ +jest.mock('../../../../../containers/metrics_source', () => ({ useSourceContext: () => ({ sourceId: 'default' }), })); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index d0aeeca9850c4..6e334f4fbca75 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -11,7 +11,7 @@ import { first } from 'lodash'; import { getCustomMetricLabel } from '../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../common/http_api'; import { withTheme, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { findInventoryModel } from '../../../../../../common/inventory_models'; import { InventoryItemType, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index d12bef2f3cdc0..e74abb2ecc459 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -13,7 +13,7 @@ import { useEffect, useState } from 'react'; import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { useHTTPRequest } from '../../../../hooks/use_http_request'; -import { useSourceContext } from '../../../../containers/source'; +import { useSourceContext } from '../../../../containers/metrics_source'; export interface SortBy { name: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts index 8d7e516d50b57..cc1108cb91e6d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -17,7 +17,7 @@ jest.mock('react-router-dom', () => ({ }), })); -jest.mock('../../../../containers/source', () => ({ +jest.mock('../../../../containers/metrics_source', () => ({ useSourceContext: () => ({ createDerivedIndexPattern: () => 'jestbeat-*', }), diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 30c15410e1199..90cf96330e758 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -13,7 +13,7 @@ import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { useUrlState } from '../../../../utils/use_url_state'; -import { useSourceContext } from '../../../../containers/source'; +import { useSourceContext } from '../../../../containers/metrics_source'; import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; import { esKuery } from '../../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 6b980d33c2559..57073fee13c18 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -17,8 +17,8 @@ import { ColumnarPage } from '../../../components/page'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; -import { Source } from '../../../containers/source'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration/view_source_configuration_button'; +import { Source } from '../../../containers/metrics_source'; import { useTrackPageview } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Layout } from './components/layout'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts index 1e315f95dbd7c..dbe45a387891c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts @@ -14,7 +14,6 @@ const options: InfraWaffleMapOptions = { container: 'container.id', pod: 'kubernetes.pod.uid', host: 'host.name', - message: ['@message'], timestamp: '@timestanp', tiebreaker: '@timestamp', }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index 6b9912346f396..2a436eac30b2c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/e import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; -import { ViewSourceConfigurationButton } from '../../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../../components/source_configuration/view_source_configuration_button'; import { useLinkProps } from '../../../../hooks/use_link_props'; interface InvalidNodeErrorProps { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index d174707d8b6c9..13fa5cf1f0667 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -17,7 +17,7 @@ import { Header } from '../../../components/header'; import { ColumnarPage, PageContent } from '../../../components/page'; import { withMetricPageProviders } from './page_providers'; import { useMetadata } from './hooks/use_metadata'; -import { Source } from '../../../containers/source'; +import { Source } from '../../../containers/metrics_source'; import { InfraLoadingPanel } from '../../../components/loading'; import { findInventoryModel } from '../../../../common/inventory_models'; import { NavItem } from './lib/side_nav_context'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx index ac90e488cea94..c4e1b6bf8ef16 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx @@ -7,7 +7,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; -import { Source } from '../../../containers/source'; +import { Source } from '../../../containers/metrics_source'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = (Component: React.ComponentType) => ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 442382010d78c..35265f0a462cf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/charts'; import { first, last } from 'lodash'; import moment from 'moment'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, @@ -47,7 +47,7 @@ interface Props { options: MetricsExplorerOptions; chartOptions: MetricsExplorerChartOptions; series: MetricsExplorerSeries; - source: InfraSourceConfiguration | undefined; + source: MetricsSourceConfigurationProperties | undefined; timeRange: MetricsExplorerTimeOptions; onTimeChange: (start: string, end: string) => void; } diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index f5970cffa157d..8f281bda0229d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import DateMath from '@elastic/datemath'; import { Capabilities } from 'src/core/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { AlertFlyout } from '../../../../alerting/metric_threshold/components/alert_flyout'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { @@ -33,14 +33,14 @@ export interface Props { options: MetricsExplorerOptions; onFilter?: (query: string) => void; series: MetricsExplorerSeries; - source?: InfraSourceConfiguration; + source?: MetricsSourceConfigurationProperties; timeRange: MetricsExplorerTimeOptions; uiCapabilities?: Capabilities; chartOptions: MetricsExplorerChartOptions; } const fieldToNodeType = ( - source: InfraSourceConfiguration, + source: MetricsSourceConfigurationProperties, groupBy: string | string[] ): InventoryItemType | undefined => { const fields = Array.isArray(groupBy) ? groupBy : [groupBy]; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx index e2e64a6758a29..68faaf1f45145 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiFlexGrid, EuiFlexItem, EuiText, EuiHorizontalRule } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerResponse } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, @@ -31,7 +31,7 @@ interface Props { onFilter: (filter: string) => void; onTimeChange: (start: string, end: string) => void; data: MetricsExplorerResponse | null; - source: InfraSourceConfiguration | undefined; + source: MetricsSourceConfigurationProperties | undefined; timeRange: MetricsExplorerTimeOptions; } export const MetricsExplorerCharts = ({ diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index d2eeada219fa4..1a549041823ec 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -8,7 +8,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from '@elastic/safer-lodash-set'; -import { InfraSourceConfiguration } from '../../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../../common/metrics_sources'; import { colorTransformer, Color } from '../../../../../../common/color_palette'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { @@ -143,7 +143,7 @@ const createTSVBIndexPattern = (alias: string) => { }; export const createTSVBLink = ( - source: InfraSourceConfiguration | undefined, + source: MetricsSourceConfigurationProperties | undefined, options: MetricsExplorerOptions, series: MetricsExplorerSeries, timeRange: MetricsExplorerTimeOptions, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index eb5a4633d4fa9..a304c81ca1298 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -7,7 +7,7 @@ import { useState, useCallback, useContext } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerMetric, MetricsExplorerAggregation, @@ -28,7 +28,7 @@ export interface MetricExplorerViewState { } export const useMetricsExplorerState = ( - source: InfraSourceConfiguration, + source: MetricsSourceConfigurationProperties, derivedIndexPattern: IIndexPattern, shouldLoadImmediately = true ) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index 3d09a907be12f..9a5e5fcf39ce4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -22,7 +22,7 @@ import { import { MetricsExplorerOptions, MetricsExplorerTimeOptions } from './use_metrics_explorer_options'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { HttpHandler } from 'kibana/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; const mockedFetch = jest.fn(); @@ -38,7 +38,7 @@ const renderUseMetricsExplorerDataHook = () => { return renderHook( (props: { options: MetricsExplorerOptions; - source: InfraSourceConfiguration | undefined; + source: MetricsSourceConfigurationProperties | undefined; derivedIndexPattern: IIndexPattern; timeRange: MetricsExplorerTimeOptions; afterKey: string | null | Record; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index b6620e963217d..6689aedcd7209 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -9,7 +9,7 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState, useCallback } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerResponse, metricsExplorerResponseRT, @@ -25,7 +25,7 @@ function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOpt export function useMetricsExplorerData( options: MetricsExplorerOptions, - source: InfraSourceConfiguration | undefined, + source: MetricsSourceConfigurationProperties | undefined, derivedIndexPattern: IIndexPattern, timerange: MetricsExplorerTimeOptions, afterKey: string | null | Record, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index 3eb9bbacddd2e..0d1ac47812577 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -9,7 +9,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSourceConfiguration } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; import { useTrackPageview } from '../../../../../observability/public'; import { DocumentTitle } from '../../../components/document_title'; import { NoData } from '../../../components/empty_states'; @@ -19,7 +19,7 @@ import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; import { useSavedViewContext } from '../../../containers/saved_view/saved_view'; interface MetricsExplorerPageProps { - source: InfraSourceConfiguration; + source: MetricsSourceConfigurationProperties; derivedIndexPattern: IIndexPattern; } diff --git a/x-pack/plugins/infra/public/pages/metrics/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx index c9be4abcf9e5f..c54725ab39754 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings.tsx @@ -8,7 +8,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; +import { SourceConfigurationSettings } from './settings/source_configuration_settings'; export const MetricsSettingsPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; diff --git a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/fields_configuration_panel.tsx similarity index 98% rename from x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/fields_configuration_panel.tsx index 2a8abdbc04f8e..7026f372ec7ff 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/fields_configuration_panel.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InputFieldProps } from './input_fields'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface FieldsConfigurationPanelProps { containerFieldProps: InputFieldProps; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts similarity index 91% rename from x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts rename to x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts index b4dede79d11f2..ad26c1b13b0e1 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts @@ -11,16 +11,14 @@ import { createInputFieldProps, createInputRangeFieldProps, validateInputFieldNotEmpty, -} from './input_fields'; +} from '../../../components/source_configuration/input_fields'; interface FormState { name: string; description: string; metricAlias: string; - logAlias: string; containerField: string; hostField: string; - messageField: string[]; podField: string; tiebreakerField: string; timestampField: string; @@ -56,16 +54,6 @@ export const useIndicesConfigurationFormState = ({ }), [formState.name] ); - const logAliasFieldProps = useMemo( - () => - createInputFieldProps({ - errors: validateInputFieldNotEmpty(formState.logAlias), - name: 'logAlias', - onChange: (logAlias) => setFormStateChanges((changes) => ({ ...changes, logAlias })), - value: formState.logAlias, - }), - [formState.logAlias] - ); const metricAliasFieldProps = useMemo( () => createInputFieldProps({ @@ -144,7 +132,6 @@ export const useIndicesConfigurationFormState = ({ const fieldProps = useMemo( () => ({ name: nameFieldProps, - logAlias: logAliasFieldProps, metricAlias: metricAliasFieldProps, containerField: containerFieldFieldProps, hostField: hostFieldFieldProps, @@ -155,7 +142,6 @@ export const useIndicesConfigurationFormState = ({ }), [ nameFieldProps, - logAliasFieldProps, metricAliasFieldProps, containerFieldFieldProps, hostFieldFieldProps, @@ -193,11 +179,9 @@ export const useIndicesConfigurationFormState = ({ const defaultFormState: FormState = { name: '', description: '', - logAlias: '', metricAlias: '', containerField: '', hostField: '', - messageField: [], podField: '', tiebreakerField: '', timestampField: '', diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_panel.tsx similarity index 93% rename from x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_panel.tsx index cff9b78777aa3..c64ab2b0e9df5 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_panel.tsx @@ -17,8 +17,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { METRICS_INDEX_PATTERN } from '../../../common/constants'; -import { InputFieldProps } from './input_fields'; +import { METRICS_INDEX_PATTERN } from '../../../../common/constants'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface IndicesConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/ml_configuration_panel.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/ml_configuration_panel.tsx index 3bd498d460391..abf25dde0ea99 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/ml_configuration_panel.tsx @@ -13,7 +13,7 @@ import { EuiDescribedFormGroup } from '@elastic/eui'; import { EuiForm } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { InputRangeFieldProps } from './input_fields'; +import { InputRangeFieldProps } from '../../../components/source_configuration/input_fields'; interface MLConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx similarity index 57% rename from x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx index c80235137eea6..37da4bd1aa1bd 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx @@ -6,12 +6,12 @@ */ import { useCallback, useMemo } from 'react'; -import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; - +import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; import { useIndicesConfigurationFormState } from './indices_configuration_form_state'; -import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state'; -export const useSourceConfigurationFormState = (configuration?: InfraSourceConfiguration) => { +export const useSourceConfigurationFormState = ( + configuration?: MetricsSourceConfigurationProperties +) => { const indicesConfigurationFormState = useIndicesConfigurationFormState({ initialFormState: useMemo( () => @@ -19,11 +19,9 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi ? { name: configuration.name, description: configuration.description, - logAlias: configuration.logAlias, metricAlias: configuration.metricAlias, containerField: configuration.fields.container, hostField: configuration.fields.host, - messageField: configuration.fields.message, podField: configuration.fields.pod, tiebreakerField: configuration.fields.tiebreaker, timestampField: configuration.fields.timestamp, @@ -34,43 +32,26 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi ), }); - const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({ - initialFormState: useMemo( - () => - configuration - ? { - logColumns: configuration.logColumns, - } - : undefined, - [configuration] - ), - }); - - const errors = useMemo( - () => [...indicesConfigurationFormState.errors, ...logColumnsConfigurationFormState.errors], - [indicesConfigurationFormState.errors, logColumnsConfigurationFormState.errors] - ); + const errors = useMemo(() => [...indicesConfigurationFormState.errors], [ + indicesConfigurationFormState.errors, + ]); const resetForm = useCallback(() => { indicesConfigurationFormState.resetForm(); - logColumnsConfigurationFormState.resetForm(); - }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); + }, [indicesConfigurationFormState]); - const isFormDirty = useMemo( - () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, - [indicesConfigurationFormState.isFormDirty, logColumnsConfigurationFormState.isFormDirty] - ); + const isFormDirty = useMemo(() => indicesConfigurationFormState.isFormDirty, [ + indicesConfigurationFormState.isFormDirty, + ]); - const isFormValid = useMemo( - () => indicesConfigurationFormState.isFormValid && logColumnsConfigurationFormState.isFormValid, - [indicesConfigurationFormState.isFormValid, logColumnsConfigurationFormState.isFormValid] - ); + const isFormValid = useMemo(() => indicesConfigurationFormState.isFormValid, [ + indicesConfigurationFormState.isFormValid, + ]); const formState = useMemo( () => ({ name: indicesConfigurationFormState.formState.name, description: indicesConfigurationFormState.formState.description, - logAlias: indicesConfigurationFormState.formState.logAlias, metricAlias: indicesConfigurationFormState.formState.metricAlias, fields: { container: indicesConfigurationFormState.formState.containerField, @@ -79,17 +60,15 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi tiebreaker: indicesConfigurationFormState.formState.tiebreakerField, timestamp: indicesConfigurationFormState.formState.timestampField, }, - logColumns: logColumnsConfigurationFormState.formState.logColumns, anomalyThreshold: indicesConfigurationFormState.formState.anomalyThreshold, }), - [indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState] + [indicesConfigurationFormState.formState] ); const formStateChanges = useMemo( () => ({ name: indicesConfigurationFormState.formStateChanges.name, description: indicesConfigurationFormState.formStateChanges.description, - logAlias: indicesConfigurationFormState.formStateChanges.logAlias, metricAlias: indicesConfigurationFormState.formStateChanges.metricAlias, fields: { container: indicesConfigurationFormState.formStateChanges.containerField, @@ -98,25 +77,18 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi tiebreaker: indicesConfigurationFormState.formStateChanges.tiebreakerField, timestamp: indicesConfigurationFormState.formStateChanges.timestampField, }, - logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns, anomalyThreshold: indicesConfigurationFormState.formStateChanges.anomalyThreshold, }), - [ - indicesConfigurationFormState.formStateChanges, - logColumnsConfigurationFormState.formStateChanges, - ] + [indicesConfigurationFormState.formStateChanges] ); return { - addLogColumn: logColumnsConfigurationFormState.addLogColumn, - moveLogColumn: logColumnsConfigurationFormState.moveLogColumn, errors, formState, formStateChanges, isFormDirty, isFormValid, indicesConfigurationProps: indicesConfigurationFormState.fieldProps, - logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps, resetForm, }; }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx similarity index 94% rename from x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx index e63f43470497d..71fa4e7600503 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx @@ -19,15 +19,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useContext, useMemo } from 'react'; -import { Source } from '../../containers/source'; +import { Source } from '../../../containers/metrics_source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; import { IndicesConfigurationPanel } from './indices_configuration_panel'; -import { NameConfigurationPanel } from './name_configuration_panel'; +import { NameConfigurationPanel } from '../../../components/source_configuration/name_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; -import { SourceLoadingPage } from '../source_loading_page'; -import { Prompt } from '../../utils/navigation_warning_prompt'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { Prompt } from '../../../utils/navigation_warning_prompt'; import { MLConfigurationPanel } from './ml_configuration_panel'; -import { useInfraMLCapabilitiesContext } from '../../containers/ml/infra_ml_capabilities'; +import { useInfraMLCapabilitiesContext } from '../../../containers/ml/infra_ml_capabilities'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 4d70676d25e40..068abd0e0f20f 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -19,8 +19,8 @@ import type { } from '../../../plugins/triggers_actions_ui/public'; import type { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; import type { - ObservabilityPluginSetup, - ObservabilityPluginStart, + ObservabilityPublicSetup, + ObservabilityPublicStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { MlPluginStart, MlPluginSetup } from '../../ml/public'; @@ -33,7 +33,7 @@ export type InfraClientStartExports = void; export interface InfraClientSetupDeps { dataEnhanced: DataEnhancedSetup; home?: HomePublicPluginSetup; - observability: ObservabilityPluginSetup; + observability: ObservabilityPublicSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; ml: MlPluginSetup; @@ -43,7 +43,7 @@ export interface InfraClientSetupDeps { export interface InfraClientStartDeps { data: DataPublicPluginStart; dataEnhanced: DataEnhancedStart; - observability: ObservabilityPluginStart; + observability: ObservabilityPublicStart; spaces: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; usageCollection: UsageCollectionStart; diff --git a/x-pack/plugins/infra/public/utils/enzyme_helpers.tsx b/x-pack/plugins/infra/public/utils/enzyme_helpers.tsx index 124b6b8f13bf9..33fbbd03d790a 100644 --- a/x-pack/plugins/infra/public/utils/enzyme_helpers.tsx +++ b/x-pack/plugins/infra/public/utils/enzyme_helpers.tsx @@ -5,6 +5,7 @@ * 2.0. */ +// eslint-disable-next-line import/no-extraneous-dependencies import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { act as reactAct } from 'react-dom/test-utils'; diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 5369deb1034ee..43f0b12a23f23 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -6,7 +6,7 @@ */ import { encode } from 'rison-node'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public'; import { DEFAULT_SOURCE_ID } from '../../common/constants'; import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; @@ -81,7 +81,7 @@ async function fetchLogsOverview( dataPlugin: InfraClientStartDeps['data'] ): Promise { return new Promise((resolve, reject) => { - let esResponse: SearchResponse | undefined; + let esResponse: estypes.SearchResponse | undefined; dataPlugin.search .search({ @@ -99,7 +99,7 @@ async function fetchLogsOverview( (error) => reject(error), () => { if (esResponse?.aggregations) { - resolve(processLogsOverviewAggregations(esResponse!.aggregations)); + resolve(processLogsOverviewAggregations(esResponse!.aggregations as any)); } else { resolve({ stats: {}, series: {} }); } diff --git a/x-pack/plugins/infra/public/utils/source_configuration.ts b/x-pack/plugins/infra/public/utils/source_configuration.ts index b7b45d1927711..a3e1741c7590b 100644 --- a/x-pack/plugins/infra/public/utils/source_configuration.ts +++ b/x-pack/plugins/infra/public/utils/source_configuration.ts @@ -6,14 +6,14 @@ */ import { - InfraSavedSourceConfigurationColumn, - InfraSavedSourceConfigurationFields, + InfraSourceConfigurationColumn, + InfraSourceConfigurationFieldColumn, InfraSourceConfigurationMessageColumn, InfraSourceConfigurationTimestampColumn, -} from '../../common/http_api/source_api'; +} from '../../common/source_configuration/source_configuration'; -export type LogColumnConfiguration = InfraSavedSourceConfigurationColumn; -export type FieldLogColumnConfiguration = InfraSavedSourceConfigurationFields; +export type LogColumnConfiguration = InfraSourceConfigurationColumn; +export type FieldLogColumnConfiguration = InfraSourceConfigurationFieldColumn; export type MessageLogColumnConfiguration = InfraSourceConfigurationMessageColumn; export type TimestampLogColumnConfiguration = InfraSourceConfigurationTimestampColumn; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 69595c90c7911..f42207e0ad142 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -32,7 +32,7 @@ import { } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; -import { initSourceRoute } from './routes/source'; +import { initMetricsSourceConfigurationRoutes } from './routes/metrics_sources'; import { initOverviewRoute } from './routes/overview'; import { initAlertPreviewRoute } from './routes/alerting'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; @@ -50,7 +50,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetHostsAnomaliesRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); - initSourceRoute(libs); + initMetricsSourceConfigurationRoutes(libs); initValidateLogAnalysisDatasetsRoute(libs); initValidateLogAnalysisIndicesRoute(libs); initGetLogEntryExamplesRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 542413118b330..1231a19f80ca2 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -166,23 +166,6 @@ export interface InfraFieldDef { [type: string]: InfraFieldDetails; } -export interface InfraTSVBResponse { - [key: string]: InfraTSVBPanel; -} - -export interface InfraTSVBPanel { - id: string; - series: InfraTSVBSeries[]; -} - -export interface InfraTSVBSeries { - id: string; - label: string; - data: InfraTSVBDataPoint[]; -} - -export type InfraTSVBDataPoint = [number, number]; - export type InfraRouteConfig = { method: RouteMethod; } & RouteConfig; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 2cb00644f56d4..0176361ede66f 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -9,12 +9,11 @@ import { IndicesExistsAlias, IndicesGet, MlGetBuckets, - Msearch, } from '@elastic/elasticsearch/api/requestParams'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; +import { estypes } from '@elastic/elasticsearch'; import { InfraRouteConfig, - InfraTSVBResponse, InfraServerPluginSetupDeps, CallWithRequestParams, InfraDatabaseSearchResponse, @@ -34,6 +33,7 @@ import { RequestHandler } from '../../../../../../../src/core/server'; import { InfraConfig } from '../../../plugin'; import type { InfraPluginRequestHandlerContext } from '../../../types'; import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; +import { TimeseriesVisData } from '../../../../../../../src/plugins/vis_type_timeseries/server'; export class KibanaFramework { public router: IRouter; @@ -153,7 +153,7 @@ export class KibanaFramework { apiResult = elasticsearch.client.asCurrentUser.msearch({ ...params, ...frozenIndicesParams, - } as Msearch); + } as estypes.MultiSearchRequest); break; case 'fieldCaps': apiResult = elasticsearch.client.asCurrentUser.fieldCaps({ @@ -221,7 +221,7 @@ export class KibanaFramework { model: TSVBMetricModel, timerange: { min: number; max: number }, filters: any[] - ): Promise { + ): Promise { const { getVisData } = this.plugins.visTypeTimeseries; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index e390d6525cd60..6009652e2d0b0 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -21,6 +21,7 @@ import { import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework'; import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { isVisSeriesData } from '../../../../../../../src/plugins/vis_type_timeseries/server'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { private framework: KibanaFramework; @@ -34,7 +35,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { options: InfraMetricsRequestOptions, rawRequest: KibanaRequest ): Promise { - const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; + const indexPattern = `${options.sourceConfiguration.metricAlias}`; const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); const nodeField = fields.id; @@ -59,7 +60,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { return Promise.all(requests) .then((results) => { - return results.map((result) => { + return results.filter(isVisSeriesData).map((result) => { const metricIds = Object.keys(result).filter( (k) => !['type', 'uiRestrictions'].includes(k) ); @@ -112,7 +113,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ); } - const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; + const indexPattern = `${options.sourceConfiguration.metricAlias}`; const timerange = { min: options.timerange.from, max: options.timerange.to, @@ -132,7 +133,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { const calculatedInterval = await calculateMetricInterval( client, { - indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, + indexPattern: `${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, timerange: options.timerange, }, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 615de182662f1..5244b8a81e75f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -23,6 +23,7 @@ import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_ap import { InfraSource } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { getNodes } from '../../../routes/snapshot/lib/get_nodes'; +import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; type ConditionResult = InventoryMetricConditions & { shouldFire: boolean[]; @@ -36,6 +37,7 @@ export const evaluateCondition = async ( condition: InventoryMetricConditions, nodeType: InventoryItemType, source: InfraSource, + logQueryFields: LogQueryFields, esClient: ElasticsearchClient, filterQuery?: string, lookbackSize?: number @@ -58,6 +60,7 @@ export const evaluateCondition = async ( metric, timerange, source, + logQueryFields, filterQuery, customMetric ); @@ -101,12 +104,14 @@ const getData = async ( metric: SnapshotMetricType, timerange: InfraTimerangeInput, source: InfraSource, + logQueryFields: LogQueryFields, filterQuery?: string, customMetric?: SnapshotCustomMetricInput ) => { const client = async ( options: CallWithRequestParams ): Promise> => + // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required (await esClient.search(options)).body as InfraDatabaseSearchResponse; const metrics = [ @@ -123,7 +128,7 @@ const getData = async ( includeTimeseries: Boolean(timerange.lookbackSize), }; try { - const { nodes } = await getNodes(client, snapshotRequest, source); + const { nodes } = await getNodes(client, snapshotRequest, source, logQueryFields); if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 632ba9cd6f282..d775a503d1d32 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -68,12 +68,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = sourceId || 'default' ); + const logQueryFields = await libs.getLogQueryFields( + sourceId || 'default', + services.savedObjectsClient + ); + const results = await Promise.all( criteria.map((c) => evaluateCondition( c, nodeType, source, + logQueryFields, services.scopedClusterClient.asCurrentUser, filterQuery ) diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 472f9d408694c..f254f1e68ae46 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -14,10 +14,11 @@ import { isTooManyBucketsPreviewException, } from '../../../../common/alerting/metrics'; import { ElasticsearchClient } from '../../../../../../../src/core/server'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { InfraSource } from '../../../../common/source_configuration/source_configuration'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { evaluateCondition } from './evaluate_condition'; +import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -30,6 +31,7 @@ interface PreviewInventoryMetricThresholdAlertParams { esClient: ElasticsearchClient; params: InventoryMetricThresholdParams; source: InfraSource; + logQueryFields: LogQueryFields; lookback: Unit; alertInterval: string; alertThrottle: string; @@ -43,6 +45,7 @@ export const previewInventoryMetricThresholdAlert: ( esClient, params, source, + logQueryFields, lookback, alertInterval, alertThrottle, @@ -68,7 +71,7 @@ export const previewInventoryMetricThresholdAlert: ( try { const results = await Promise.all( criteria.map((c) => - evaluateCondition(c, nodeType, source, esClient, filterQuery, lookbackSize) + evaluateCondition(c, nodeType, source, logQueryFields, esClient, filterQuery, lookbackSize) ) ); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index b7d3dbb1f7adb..87150aa134837 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -11,7 +11,7 @@ import { isTooManyBucketsPreviewException, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, } from '../../../../../common/alerting/metrics'; -import { InfraSource } from '../../../../../common/http_api/source_api'; +import { InfraSource } from '../../../../../common/source_configuration/source_configuration'; import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../../utils/get_all_composite_data'; @@ -127,6 +127,7 @@ const getMetric: ( (response) => response.aggregations?.groupings?.after_key ); const compositeBuckets = (await getAllCompositeData( + // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required (body) => esClient.search({ body, index }), searchBody, bucketSelector, @@ -147,7 +148,12 @@ const getMetric: ( index, }); - return { [UNGROUPED_FACTORY_KEY]: getValuesFromAggregations(result.aggregations, aggType) }; + return { + [UNGROUPED_FACTORY_KEY]: getValuesFromAggregations( + (result.aggregations! as unknown) as Aggregation, + aggType + ), + }; } catch (e) { if (timeframe) { // This code should only ever be reached when previewing the alert, not executing it diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 064804b661b74..a4c207f4006d5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -12,7 +12,7 @@ import { isTooManyBucketsPreviewException, } from '../../../../common/alerting/metrics'; import { ElasticsearchClient } from '../../../../../../../src/core/server'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { InfraSource } from '../../../../common/source_configuration/source_configuration'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { PreviewResult } from '../common/types'; import { MetricExpressionParams } from './types'; diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts index b653351a34760..d5ffa56987666 100644 --- a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -18,21 +18,16 @@ export class InfraFieldsDomain { public async getFields( requestContext: InfraPluginRequestHandlerContext, sourceId: string, - indexType: 'LOGS' | 'METRICS' | 'ANY' + indexType: 'LOGS' | 'METRICS' ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId ); - const includeMetricIndices = ['ANY', 'METRICS'].includes(indexType); - const includeLogIndices = ['ANY', 'LOGS'].includes(indexType); const fields = await this.adapter.getIndexFields( requestContext, - [ - ...(includeMetricIndices ? [configuration.metricAlias] : []), - ...(includeLogIndices ? [configuration.logAlias] : []), - ].join(',') + indexType === 'LOGS' ? configuration.logAlias : configuration.metricAlias ); return fields; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index e3c42c4dceede..278ae0e086cfc 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -17,7 +17,7 @@ import { LogColumn, LogEntryCursor, LogEntry } from '../../../../common/log_entr import { InfraSourceConfiguration, InfraSources, - SavedSourceConfigurationFieldColumnRuntimeType, + SourceConfigurationFieldColumnRuntimeType, } from '../../sources'; import { getBuiltinRules } from '../../../services/log_entries/message/builtin_rules'; import { @@ -349,7 +349,7 @@ const getRequiredFields = ( ): string[] => { const fieldsFromCustomColumns = configuration.logColumns.reduce( (accumulatedFields, logColumn) => { - if (SavedSourceConfigurationFieldColumnRuntimeType.is(logColumn)) { + if (SourceConfigurationFieldColumnRuntimeType.is(logColumn)) { return [...accumulatedFields, logColumn.fieldColumn.field]; } return accumulatedFields; diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 65bb5f878b275..08e42279e4939 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { InfraSourceConfiguration } from '../../common/http_api/source_api'; +import { InfraSourceConfiguration } from '../../common/source_configuration/source_configuration'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; @@ -13,6 +13,7 @@ import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; import { InfraConfig } from '../plugin'; import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; +import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -25,6 +26,7 @@ export interface InfraBackendLibs extends InfraDomainLibs { framework: KibanaFramework; sources: InfraSources; sourceStatus: InfraSourceStatus; + getLogQueryFields: GetLogQueryFields; } export interface InfraConfiguration { diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index cb89c5a6b1bd3..e436ad2ba0b05 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -120,5 +120,5 @@ export const query = async ( ThrowReporter.report(HistogramResponseRT.decode(response.aggregations)); } - throw new Error('Elasticsearch responsed with an unrecoginzed format.'); + throw new Error('Elasticsearch responded with an unrecognized format.'); }; diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index 1b924619a905c..ff6d6a4f5514b 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -10,7 +10,7 @@ import { LOGS_INDEX_PATTERN, TIMESTAMP_FIELD, } from '../../../common/constants'; -import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; +import { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', diff --git a/x-pack/plugins/infra/server/lib/sources/index.ts b/x-pack/plugins/infra/server/lib/sources/index.ts index 57852f7f3e4e6..27ad665be31a9 100644 --- a/x-pack/plugins/infra/server/lib/sources/index.ts +++ b/x-pack/plugins/infra/server/lib/sources/index.ts @@ -8,4 +8,4 @@ export * from './defaults'; export { infraSourceConfigurationSavedObjectType } from './saved_object_type'; export * from './sources'; -export * from '../../../common/http_api/source_api'; +export * from '../../../common/source_configuration/source_configuration'; diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts index dbfe0f81c187a..e71994fe11517 100644 --- a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts @@ -6,7 +6,7 @@ */ import { SavedObjectMigrationFn } from 'src/core/server'; -import { InfraSourceConfiguration } from '../../../../common/http_api/source_api'; +import { InfraSourceConfiguration } from '../../../../common/source_configuration/source_configuration'; export const addNewIndexingStrategyIndexNames: SavedObjectMigrationFn< InfraSourceConfiguration, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index fe005b04978da..7abbed0a9fbdd 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -23,7 +23,7 @@ import { SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, InfraSource, -} from '../../../common/http_api/source_api'; +} from '../../../common/source_configuration/source_configuration'; import { InfraConfig } from '../../../server'; interface Libs { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index c80e012844c1e..50fec38b9f2df 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -9,7 +9,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; -import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; +import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; @@ -30,6 +30,7 @@ import { InfraSourceStatus } from './lib/source_status'; import { LogEntriesService } from './services/log_entries'; import { InfraPluginRequestHandlerContext } from './types'; import { UsageCollector } from './usage/usage_collector'; +import { createGetLogQueryFields } from './services/log_queries/get_log_query_fields'; export const config = { schema: schema.object({ @@ -123,6 +124,7 @@ export class InfraServerPlugin implements Plugin { sources, sourceStatus, ...domainLibs, + getLogQueryFields: createGetLogQueryFields(sources), }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 6622df1a8333a..4d980834d3a70 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -25,7 +25,11 @@ import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/pre import { InfraBackendLibs } from '../../lib/infra_types'; import { assertHasInfraMlPlugins } from '../../utils/request_context'; -export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { +export const initAlertPreviewRoute = ({ + framework, + sources, + getLogQueryFields, +}: InfraBackendLibs) => { framework.registerRoute( { method: 'post', @@ -77,6 +81,10 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { + const logQueryFields = await getLogQueryFields( + sourceId || 'default', + requestContext.core.savedObjects.client + ); const { nodeType, criteria, @@ -87,6 +95,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) params: { criteria, filterQuery, nodeType }, lookback, source, + logQueryFields, alertInterval, alertThrottle, alertNotifyWhen, diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/metrics_sources/index.ts similarity index 69% rename from x-pack/plugins/infra/server/routes/source/index.ts rename to x-pack/plugins/infra/server/routes/metrics_sources/index.ts index 5ab3275f9ea9e..0123e4678697c 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_sources/index.ts @@ -8,63 +8,49 @@ import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; import { createValidationFunction } from '../../../common/runtime_types'; -import { - InfraSourceStatus, - SavedSourceConfigurationRuntimeType, - SourceResponseRuntimeType, -} from '../../../common/http_api/source_api'; import { InfraBackendLibs } from '../../lib/infra_types'; import { hasData } from '../../lib/sources/has_data'; import { createSearchClient } from '../../lib/create_search_client'; import { AnomalyThresholdRangeError } from '../../lib/sources/errors'; +import { + partialMetricsSourceConfigurationPropertiesRT, + metricsSourceConfigurationResponseRT, + MetricsSourceStatus, +} from '../../../common/metrics_sources'; -const typeToInfraIndexType = (value: string | undefined) => { - switch (value) { - case 'metrics': - return 'METRICS'; - case 'logs': - return 'LOGS'; - default: - return 'ANY'; - } -}; - -export const initSourceRoute = (libs: InfraBackendLibs) => { +export const initMetricsSourceConfigurationRoutes = (libs: InfraBackendLibs) => { const { framework } = libs; framework.registerRoute( { method: 'get', - path: '/api/metrics/source/{sourceId}/{type?}', + path: '/api/metrics/source/{sourceId}', validate: { params: schema.object({ sourceId: schema.string(), - type: schema.string(), }), }, }, async (requestContext, request, response) => { - const { type, sourceId } = request.params; + const { sourceId } = request.params; - const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ + const [source, metricIndicesExist, indexFields] = await Promise.all([ libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), + libs.fields.getFields(requestContext, sourceId, 'METRICS'), ]); if (!source) { return response.notFound(); } - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', + const status: MetricsSourceStatus = { metricIndicesExist, indexFields, }; return response.ok({ - body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), + body: metricsSourceConfigurationResponseRT.encode({ source: { ...source, status } }), }); } ); @@ -77,7 +63,7 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { params: schema.object({ sourceId: schema.string(), }), - body: createValidationFunction(SavedSourceConfigurationRuntimeType), + body: createValidationFunction(partialMetricsSourceConfigurationPropertiesRT), }, }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { @@ -110,20 +96,18 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { patchedSourceConfigurationProperties )); - const [logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), + const [metricIndicesExist, indexFields] = await Promise.all([ libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType('metrics')), + libs.fields.getFields(requestContext, sourceId, 'METRICS'), ]); - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', + const status: MetricsSourceStatus = { metricIndicesExist, indexFields, }; return response.ok({ - body: SourceResponseRuntimeType.encode({ + body: metricsSourceConfigurationResponseRT.encode({ source: { ...patchedSourceConfiguration, status }, }), }); @@ -154,25 +138,23 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { framework.registerRoute( { method: 'get', - path: '/api/metrics/source/{sourceId}/{type}/hasData', + path: '/api/metrics/source/{sourceId}/hasData', validate: { params: schema.object({ sourceId: schema.string(), - type: schema.string(), }), }, }, async (requestContext, request, response) => { - const { type, sourceId } = request.params; + const { sourceId } = request.params; const client = createSearchClient(requestContext, framework); const source = await libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId ); - const indexPattern = - type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; - const results = await hasData(indexPattern, client); + + const results = await hasData(source.configuration.metricAlias, client); return response.ok({ body: { hasData: results }, diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index aaf23085d0d60..cbadd26ccd4bf 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -41,9 +41,15 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { snapshotRequest.sourceId ); + const logQueryFields = await libs.getLogQueryFields( + snapshotRequest.sourceId, + requestContext.core.savedObjects.client + ); + UsageCollector.countNode(snapshotRequest.nodeType); const client = createSearchClient(requestContext, framework); - const snapshotResponse = await getNodes(client, snapshotRequest, source); + + const snapshotResponse = await getNodes(client, snapshotRequest, source, logQueryFields); return response.ok({ body: SnapshotNodeResponseRT.encode(snapshotResponse), diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts deleted file mode 100644 index 85c1ece1ca042..0000000000000 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SnapshotRequest } from '../../../../common/http_api'; -import { InfraSource } from '../../../lib/sources'; - -export const calculateIndexPatterBasedOnMetrics = ( - options: SnapshotRequest, - source: InfraSource -) => { - const { metrics } = options; - if (metrics.every((m) => m.type === 'logRate')) { - return source.configuration.logAlias; - } - if (metrics.some((m) => m.type === 'logRate')) { - return `${source.configuration.logAlias},${source.configuration.metricAlias}`; - } - return source.configuration.metricAlias; -}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index 9dec21d3ab1c7..ff3cf048b99de 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -12,16 +12,24 @@ import { transformRequestToMetricsAPIRequest } from './transform_request_to_metr import { queryAllData } from './query_all_data'; import { transformMetricsApiResponseToSnapshotResponse } from './trasform_metrics_ui_response'; import { copyMissingMetrics } from './copy_missing_metrics'; +import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; -export const getNodes = async ( +export interface SourceOverrides { + indexPattern: string; + timestamp: string; +} + +const transformAndQueryData = async ( client: ESSearchClient, snapshotRequest: SnapshotRequest, - source: InfraSource + source: InfraSource, + sourceOverrides?: SourceOverrides ) => { const metricsApiRequest = await transformRequestToMetricsAPIRequest( client, source, - snapshotRequest + snapshotRequest, + sourceOverrides ); const metricsApiResponse = await queryAllData(client, metricsApiRequest); const snapshotResponse = transformMetricsApiResponseToSnapshotResponse( @@ -32,3 +40,59 @@ export const getNodes = async ( ); return copyMissingMetrics(snapshotResponse); }; + +export const getNodes = async ( + client: ESSearchClient, + snapshotRequest: SnapshotRequest, + source: InfraSource, + logQueryFields: LogQueryFields +) => { + let nodes; + + if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) { + // *Only* the log rate metric has been requested + if (snapshotRequest.metrics.length === 1) { + nodes = await transformAndQueryData(client, snapshotRequest, source, logQueryFields); + } else { + // A scenario whereby a single host might be shipping metrics and logs. + const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter( + (metric) => metric.type !== 'logRate' + ); + const nodesWithoutLogsMetrics = await transformAndQueryData( + client, + { ...snapshotRequest, metrics: metricsWithoutLogsMetrics }, + source + ); + const logRateNodes = await transformAndQueryData( + client, + { ...snapshotRequest, metrics: [{ type: 'logRate' }] }, + source, + logQueryFields + ); + // Merge nodes where possible - e.g. a single host is shipping metrics and logs + const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => { + const logRateNode = logRateNodes.nodes.find( + (_logRateNode) => node.name === _logRateNode.name + ); + if (logRateNode) { + // Remove this from the "leftovers" + logRateNodes.nodes.filter((_node) => _node.name !== logRateNode.name); + } + return logRateNode + ? { + ...node, + metrics: [...node.metrics, ...logRateNode.metrics], + } + : node; + }); + nodes = { + ...nodesWithoutLogsMetrics, + nodes: [...mergedNodes, ...logRateNodes.nodes], + }; + } + } else { + nodes = await transformAndQueryData(client, snapshotRequest, source); + } + + return nodes; +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 8804121fc4167..128137efa272e 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -12,13 +12,14 @@ import { InfraSource } from '../../../lib/sources'; import { createTimeRangeWithInterval } from './create_timerange_with_interval'; import { parseFilterQuery } from '../../../utils/serialized_query'; import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapshot_metrics_to_metrics_api_metrics'; -import { calculateIndexPatterBasedOnMetrics } from './calculate_index_pattern_based_on_metrics'; import { META_KEY } from './constants'; +import { SourceOverrides } from './get_nodes'; export const transformRequestToMetricsAPIRequest = async ( client: ESSearchClient, source: InfraSource, - snapshotRequest: SnapshotRequest + snapshotRequest: SnapshotRequest, + sourceOverrides?: SourceOverrides ): Promise => { const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, { ...snapshotRequest, @@ -27,9 +28,9 @@ export const transformRequestToMetricsAPIRequest = async ( }); const metricsApiRequest: MetricsAPIRequest = { - indexPattern: calculateIndexPatterBasedOnMetrics(snapshotRequest, source), + indexPattern: sourceOverrides?.indexPattern ?? source.configuration.metricAlias, timerange: { - field: source.configuration.fields.timestamp, + field: sourceOverrides?.timestamp ?? source.configuration.fields.timestamp, from: timeRangeWithIntervalApplied.from, to: timeRangeWithIntervalApplied.to, interval: timeRangeWithIntervalApplied.interval, @@ -74,7 +75,7 @@ export const transformRequestToMetricsAPIRequest = async ( top_hits: { size: 1, _source: [inventoryFields.name], - sort: [{ [source.configuration.fields.timestamp]: 'desc' }], + sort: [{ [sourceOverrides?.timestamp ?? source.configuration.fields.timestamp]: 'desc' }], }, }, }, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts index 161685aac29ad..32bb0596ab561 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts @@ -98,6 +98,7 @@ export const logEntriesSearchStrategyProvider = ({ map( ([{ configuration }, messageFormattingRules]): IEsSearchRequest => { return { + // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record params: createGetLogEntriesQuery( configuration.logAlias, params.startTimestamp, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts index 85eacba823b2b..b6073f1bbe4c9 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts @@ -65,6 +65,7 @@ export const logEntrySearchStrategyProvider = ({ sourceConfiguration$.pipe( map( ({ configuration }): IEsSearchRequest => ({ + // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record params: createGetLogEntryQuery( configuration.logAlias, params.logEntryId, diff --git a/x-pack/plugins/infra/server/services/log_entries/message/builtin_rules/generic.ts b/x-pack/plugins/infra/server/services/log_entries/message/builtin_rules/generic.ts index b06752ee0a80d..c16d65a75b3e0 100644 --- a/x-pack/plugins/infra/server/services/log_entries/message/builtin_rules/generic.ts +++ b/x-pack/plugins/infra/server/services/log_entries/message/builtin_rules/generic.ts @@ -45,6 +45,37 @@ export const getGenericRules = (genericMessageFields: string[]) => [ ]; const createGenericRulesForField = (fieldName: string) => [ + { + when: { + exists: ['event.dataset', 'log.level', fieldName, 'error.stack_trace.text'], + }, + format: [ + { + constant: '[', + }, + { + field: 'event.dataset', + }, + { + constant: '][', + }, + { + field: 'log.level', + }, + { + constant: '] ', + }, + { + field: fieldName, + }, + { + constant: '\n', + }, + { + field: 'error.stack_trace.text', + }, + ], + }, { when: { exists: ['event.dataset', 'log.level', fieldName], @@ -70,6 +101,31 @@ const createGenericRulesForField = (fieldName: string) => [ }, ], }, + { + when: { + exists: ['log.level', fieldName, 'error.stack_trace.text'], + }, + format: [ + { + constant: '[', + }, + { + field: 'log.level', + }, + { + constant: '] ', + }, + { + field: fieldName, + }, + { + constant: '\n', + }, + { + field: 'error.stack_trace.text', + }, + ], + }, { when: { exists: ['log.level', fieldName], @@ -89,6 +145,22 @@ const createGenericRulesForField = (fieldName: string) => [ }, ], }, + { + when: { + exists: [fieldName, 'error.stack_trace.text'], + }, + format: [ + { + field: fieldName, + }, + { + constant: '\n', + }, + { + field: 'error.stack_trace.text', + }, + ], + }, { when: { exists: [fieldName], diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts b/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts index aa640f106d1ee..6ae7232d77a17 100644 --- a/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts +++ b/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RequestParams } from '@elastic/elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import * as rt from 'io-ts'; import { LogEntryAfterCursor, @@ -31,7 +31,7 @@ export const createGetLogEntriesQuery = ( fields: string[], query?: JsonObject, highlightTerm?: string -): RequestParams.AsyncSearchSubmit> => { +): estypes.AsyncSearchSubmitRequest => { const sortDirection = getSortDirection(cursor); const highlightQuery = createHighlightQuery(highlightTerm, fields); @@ -51,6 +51,7 @@ export const createGetLogEntriesQuery = ( ], }, }, + // @ts-expect-error @elastic/elasticsearch doesn't declare body.fields on AsyncSearchSubmitRequest fields, _source: false, ...createSortClause(sortDirection, timestampField, tiebreakerField), diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts index 51714be775e97..85af8b92fe080 100644 --- a/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts +++ b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RequestParams } from '@elastic/elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import * as rt from 'io-ts'; import { jsonArrayRT } from '../../../../common/typed_json'; import { @@ -18,7 +18,7 @@ export const createGetLogEntryQuery = ( logEntryId: string, timestampField: string, tiebreakerField: string -): RequestParams.AsyncSearchSubmit> => ({ +): estypes.AsyncSearchSubmitRequest => ({ index: logEntryIndex, terminate_after: 1, track_scores: false, @@ -30,6 +30,7 @@ export const createGetLogEntryQuery = ( values: [logEntryId], }, }, + // @ts-expect-error @elastic/elasticsearch doesn't declare body.fields on AsyncSearchSubmitRequest fields: ['*'], sort: [{ [timestampField]: 'desc' }, { [tiebreakerField]: 'desc' }], _source: false, diff --git a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts new file mode 100644 index 0000000000000..9497a8b442768 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.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 { SavedObjectsClientContract } from 'src/core/server'; +import { InfraSources } from '../../lib/sources'; + +// NOTE: TEMPORARY: This will become a subset of the new resolved KIP compatible log source configuration. +export interface LogQueryFields { + indexPattern: string; + timestamp: string; +} + +// NOTE: TEMPORARY: This will become a subset of the new resolved KIP compatible log source configuration. +export const createGetLogQueryFields = (sources: InfraSources) => { + return async ( + sourceId: string, + savedObjectsClient: SavedObjectsClientContract + ): Promise => { + const source = await sources.getSourceConfiguration(savedObjectsClient, sourceId); + + return { + indexPattern: source.configuration.logAlias, + timestamp: source.configuration.fields.timestamp, + }; + }; +}; + +export type GetLogQueryFields = ReturnType; diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts index 1c51a5549cb41..5cae015861946 100644 --- a/x-pack/plugins/infra/server/types.ts +++ b/x-pack/plugins/infra/server/types.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { DataRequestHandlerContext } from '../../../../src/plugins/data/server'; +import type { RequestHandlerContext } from 'src/core/server'; +import type { SearchRequestHandlerContext } from '../../../../src/plugins/data/server'; import { MlPluginSetup } from '../../ml/server'; export type MlSystem = ReturnType; @@ -26,6 +27,7 @@ export type InfraRequestHandlerContext = InfraMlRequestHandlerContext & /** * @internal */ -export interface InfraPluginRequestHandlerContext extends DataRequestHandlerContext { +export interface InfraPluginRequestHandlerContext extends RequestHandlerContext { infra: InfraRequestHandlerContext; + search: SearchRequestHandlerContext; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx new file mode 100644 index 0000000000000..c6449dbd7a93e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the Bytes processor when saved +const defaultBytesParameters = { + ignore_failure: undefined, + description: undefined, +}; + +const BYTES_TYPE = 'bytes'; + +describe('Processor: Bytes', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + testBed.component.update(); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { addProcessor, saveNewProcessor, addProcessorType }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Click submit button without entering any fields + await saveNewProcessor(); + + // Expect form error as a processor type is required + expect(form.getErrorsMessages()).toEqual(['A type is required.']); + + // Add type (the other fields are not visible until a type is selected) + await addProcessorType(BYTES_TYPE); + + // Click submit button with only the type defined + await saveNewProcessor(); + + // Expect form error as "field" is required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('saves with default parameter values', async () => { + const { + actions: { addProcessor, saveNewProcessor, addProcessorType }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await addProcessorType(BYTES_TYPE); + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, BYTES_TYPE); + expect(processors[0].bytes).toEqual({ + field: 'field_1', + ...defaultBytesParameters, + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { addProcessor, addProcessorType, saveNewProcessor }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await addProcessorType(BYTES_TYPE); + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, BYTES_TYPE); + expect(processors[0].bytes).toEqual({ + description: undefined, + field: 'field_1', + ignore_failure: undefined, + target_field: 'target_field', + ignore_missing: true, + tag: undefined, + if: undefined, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index c08627de636d7..8340cf45b1f1b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -90,9 +90,9 @@ const createActions = (testBed: TestBed) => { component.update(); }, - async addProcessorType({ type, label }: { type: string; label: string }) { + async addProcessorType(type: string) { await act(async () => { - find('processorTypeSelector.input').simulate('change', [{ value: type, label }]); + find('processorTypeSelector.input').simulate('change', [{ value: type }]); }); component.update(); }, @@ -127,12 +127,19 @@ export const setupEnvironment = () => { }; }; +export const getProcessorValue = (onUpdate: jest.Mock, type: string) => { + const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; + const { processors } = onUpdateResult.getData(); + return processors; +}; + type TestSubject = | 'addProcessorForm.submitButton' | 'addProcessorButton' | 'addProcessorForm.submitButton' | 'processorTypeSelector.input' | 'fieldNameField.input' + | 'ignoreMissingSwitch.input' | 'targetField.input' | 'keepOriginalField.input' | 'removeIfSuccessfulField.input'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx new file mode 100644 index 0000000000000..de0061dcb0407 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult } from './processor.helpers'; + +describe('Processor: Bytes', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + testBed.component.update(); + }); + + test('Prevents form submission if processor type not selected', async () => { + const { + actions: { addProcessor, saveNewProcessor }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Click submit button without entering any fields + await saveNewProcessor(); + + // Expect form error as a processor type is required + expect(form.getErrorsMessages()).toEqual(['A type is required.']); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx index 41078b7e96df9..573adad3247f5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; // Default parameter values automatically added to the URI parts processor when saved const defaultUriPartsParameters = { @@ -16,6 +16,8 @@ const defaultUriPartsParameters = { description: undefined, }; +const URI_PARTS_TYPE = 'uri_parts'; + describe('Processor: URI parts', () => { let onUpdate: jest.Mock; let testBed: SetupResult; @@ -51,14 +53,9 @@ describe('Processor: URI parts', () => { // Open flyout to add new processor addProcessor(); - // Click submit button without entering any fields - await saveNewProcessor(); - - // Expect form error as a processor type is required - expect(form.getErrorsMessages()).toEqual(['A type is required.']); // Add type (the other fields are not visible until a type is selected) - await addProcessorType({ type: 'uri_parts', label: 'URI parts' }); + await addProcessorType(URI_PARTS_TYPE); // Click submit button with only the type defined await saveNewProcessor(); @@ -76,14 +73,13 @@ describe('Processor: URI parts', () => { // Open flyout to add new processor addProcessor(); // Add type (the other fields are not visible until a type is selected) - await addProcessorType({ type: 'uri_parts', label: 'URI parts' }); + await addProcessorType(URI_PARTS_TYPE); // Add "field" value (required) form.setInputValue('fieldNameField.input', 'field_1'); // Save the field await saveNewProcessor(); - const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; - const { processors } = onUpdateResult.getData(); + const processors = getProcessorValue(onUpdate, URI_PARTS_TYPE); expect(processors[0].uri_parts).toEqual({ field: 'field_1', ...defaultUriPartsParameters, @@ -99,7 +95,7 @@ describe('Processor: URI parts', () => { // Open flyout to add new processor addProcessor(); // Add type (the other fields are not visible until a type is selected) - await addProcessorType({ type: 'uri_parts', label: 'URI parts' }); + await addProcessorType(URI_PARTS_TYPE); // Add "field" value (required) form.setInputValue('fieldNameField.input', 'field_1'); @@ -111,8 +107,7 @@ describe('Processor: URI parts', () => { // Save the field with new changes await saveNewProcessor(); - const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; - const { processors } = onUpdateResult.getData(); + const processors = getProcessorValue(onUpdate, URI_PARTS_TYPE); expect(processors[0].uri_parts).toEqual({ description: undefined, field: 'field_1', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx index 82e086102b488..744e9798c4fb0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx @@ -50,5 +50,6 @@ export const IgnoreMissingField: FunctionComponent = (props) => ( config={{ ...fieldsConfig.ignore_missing, ...props }} component={ToggleField} path="fields.ignore_missing" + data-test-subj="ignoreMissingSwitch" /> ); diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 38bcf8a377bf2..20bf349f6b13a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -72,7 +72,7 @@ const { TopNavMenu } = navigationStartMock.ui; function createMockFrame(): jest.Mocked { return { - mount: jest.fn((el, props) => {}), + mount: jest.fn(async (el, props) => {}), unmount: jest.fn(() => {}), }; } diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap index 7aa838021f2a8..7de406aee2534 100644 --- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap +++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -1,24 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DragDrop defined dropType is reflected in the className 1`] = ` - + +
`; -exports[`DragDrop items that has dropType=undefined get special styling when another item is dragged 1`] = ` - + + `; exports[`DragDrop renders if nothing is being dragged 1`] = ` diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 961f7ee0ec400..57ebe79af2219 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -3,7 +3,9 @@ .lnsDragDrop { user-select: none; - transition: background-color $euiAnimSpeedFast ease-in-out, border-color $euiAnimSpeedFast ease-in-out; + transition: $euiAnimSpeedFast ease-in-out; + transition-property: background-color, border-color, opacity; + z-index: $euiZLevel1; } .lnsDragDrop_ghost { @@ -16,7 +18,7 @@ left: 0; opacity: .9; transform: translate(-12px, 8px); - z-index: $euiZLevel2; + z-index: $euiZLevel3; pointer-events: none; box-shadow: 0 0 0 $euiFocusRingSize $euiFocusRingColor; } @@ -56,6 +58,7 @@ // Drop area while hovering with item .lnsDragDrop-isActiveDropTarget { + z-index: $euiZLevel3; @include lnsDroppableActiveHover; } @@ -81,6 +84,16 @@ } } +.lnsDragDrop__container { + position: relative; + width: 100%; + height: 100%; + + &.lnsDragDrop__container-active { + z-index: $euiZLevel3; + } +} + .lnsDragDrop__reorderableDrop { position: absolute; width: 100%; @@ -92,6 +105,14 @@ transform: translateY(0); transition: transform $euiAnimSpeedFast ease-in-out; pointer-events: none; + + .lnsDragDrop-isDropTarget { + @include lnsDraggable; + } + + .lnsDragDrop-isActiveDropTarget { + z-index: $euiZLevel3; + } } .lnsDragDrop-translatableDrag { @@ -118,10 +139,6 @@ // Draggable item when it is moving .lnsDragDrop-isHidden { opacity: 0; -} - -.lnsDragDrop-isHidden-noFocus { - opacity: 0; .lnsDragDrop__keyboardHandler { &:focus, &:focus-within { @@ -129,3 +146,60 @@ } } } + +.lnsDragDrop__extraDrops { + opacity: 0; + visibility: hidden; + position: absolute; + z-index: $euiZLevel2; + right: calc(100% + #{$euiSizeS}); + top: 0; + transition: opacity $euiAnimSpeedFast ease-in-out; + width:100%; +} + +.lnsDragDrop__extraDrops-visible { + opacity: 1; + visibility: visible; +} + +.lnsDragDrop__diamondPath { + position: absolute; + width: 30%; + top: 0; + left: -$euiSize; + z-index: $euiZLevel0; +} + +.lnsDragDrop__extraDropWrapper { + position: relative; + width: 100%; + height: 100%; + background: $euiColorLightestShade; + padding: $euiSizeXS; + border-radius: 0; + &:first-child, &:first-child .lnsDragDrop__extraDrop { + border-top-left-radius: $euiSizeXS; + border-top-right-radius: $euiSizeXS; + } + &:last-child, &:last-child .lnsDragDrop__extraDrop { + border-bottom-left-radius: $euiSizeXS; + border-bottom-right-radius: $euiSizeXS; + } +} + +// collapse borders +.lnsDragDrop__extraDropWrapper + .lnsDragDrop__extraDropWrapper { + margin-top: -1px; +} + +.lnsDragDrop__extraDrop { + position: relative; + height: $euiSizeXS * 10; + min-width: $euiSize * 7; + color: $euiColorSuccessText; + padding: $euiSizeXS; + &.lnsDragDrop-incompatibleExtraDrop { + color: $euiColorWarningText; + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index dd1e351b824fe..e582c4318afc3 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, mount } from 'enzyme'; +import { render, mount, ReactWrapper } from 'enzyme'; import { DragDrop } from './drag_drop'; import { ChildDragDropProvider, @@ -39,7 +39,11 @@ describe('DragDrop', () => { registerDropTarget: jest.fn(), }; - const value = { id: '1', humanData: { label: 'hello' } }; + const value = { + id: '1', + humanData: { label: 'hello', groupLabel: 'X', position: 1, canSwap: true, canDuplicate: true }, + }; + test('renders if nothing is being dragged', () => { const component = render( @@ -53,17 +57,17 @@ describe('DragDrop', () => { test('dragover calls preventDefault if dropType is defined', () => { const preventDefault = jest.fn(); const component = mount( - + ); - component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); + component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('dragover', { preventDefault }); expect(preventDefault).toBeCalled(); }); - test('dragover does not call preventDefault if dropType is undefined', () => { + test('dragover does not call preventDefault if dropTypes is undefined', () => { const preventDefault = jest.fn(); const component = mount( @@ -71,7 +75,7 @@ describe('DragDrop', () => { ); - component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); + component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('dragover', { preventDefault }); expect(preventDefault).not.toBeCalled(); }); @@ -85,7 +89,7 @@ describe('DragDrop', () => { ); - component.find('[data-test-subj="lnsDragDrop"]').simulate('mousedown'); + component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('mousedown'); expect(global.getSelection).toBeCalled(); expect(removeAllRanges).toBeCalled(); }); @@ -107,9 +111,11 @@ describe('DragDrop', () => { ); - component.find('[data-test-subj="lnsDragDrop"]').simulate('dragstart', { dataTransfer }); + component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('dragstart', { dataTransfer }); - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); expect(dataTransfer.setData).toBeCalledWith('text', 'hello'); expect(setDragging).toBeCalledWith({ ...value }); @@ -128,15 +134,15 @@ describe('DragDrop', () => { dragging={{ id: '2', humanData: { label: 'Label1' } }} setDragging={setDragging} > - + ); - component - .find('[data-test-subj="lnsDragDrop"]') - .simulate('drop', { preventDefault, stopPropagation }); + const dragDrop = component.find('[data-test-subj="lnsDragDrop"]').at(0); + dragDrop.simulate('dragOver'); + dragDrop.simulate('drop', { preventDefault, stopPropagation }); expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); @@ -144,7 +150,7 @@ describe('DragDrop', () => { expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'Label1' } }, 'field_add'); }); - test('drop function is not called on dropType undefined', async () => { + test('drop function is not called on dropTypes undefined', async () => { const preventDefault = jest.fn(); const stopPropagation = jest.fn(); const setDragging = jest.fn(); @@ -156,29 +162,29 @@ describe('DragDrop', () => { dragging={{ id: 'hi', humanData: { label: 'Label1' } }} setDragging={setDragging} > - + ); - component - .find('[data-test-subj="lnsDragDrop"]') - .simulate('drop', { preventDefault, stopPropagation }); + const dragDrop = component.find('[data-test-subj="lnsDragDrop"]').at(0); + dragDrop.simulate('dragover'); + dragDrop.simulate('drop', { preventDefault, stopPropagation }); - expect(preventDefault).toBeCalled(); - expect(stopPropagation).toBeCalled(); - expect(setDragging).toBeCalledWith(undefined); + expect(preventDefault).not.toHaveBeenCalled(); + expect(stopPropagation).not.toHaveBeenCalled(); + expect(setDragging).not.toHaveBeenCalled(); expect(onDrop).not.toHaveBeenCalled(); }); - test('defined dropType is reflected in the className', () => { + test('defined dropTypes is reflected in the className', () => { const component = render( { throw x; }} - dropType="field_add" + dropTypes={['field_add']} value={value} order={[2, 0, 1, 0]} > @@ -189,7 +195,7 @@ describe('DragDrop', () => { expect(component).toMatchSnapshot(); }); - test('items that has dropType=undefined get special styling when another item is dragged', () => { + test('items that has dropTypes=undefined get special styling when another item is dragged', () => { const component = mount( @@ -198,7 +204,7 @@ describe('DragDrop', () => { {}} - dropType={undefined} + dropTypes={undefined} value={{ id: '2', humanData: { label: 'label2' } }} > @@ -235,7 +241,7 @@ describe('DragDrop', () => { order={[2, 0, 1, 0]} value={value} onDrop={(x: unknown) => {}} - dropType="field_add" + dropTypes={['field_add']} getAdditionalClassesOnEnter={getAdditionalClassesOnEnter} getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} > @@ -248,11 +254,14 @@ describe('DragDrop', () => { .find('[data-test-subj="lnsDragDrop"]') .first() .simulate('dragstart', { dataTransfer }); - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); expect(setA11yMessage).toBeCalledWith('Lifted ignored'); - component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); - component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); + const dragDrop = component.find('[data-test-subj="lnsDragDrop"]').at(1); + dragDrop.simulate('dragOver'); + dragDrop.simulate('drop'); expect(component.find('.additional')).toHaveLength(0); }); @@ -287,7 +296,7 @@ describe('DragDrop', () => { order={[2, 0, 1, 0]} value={value} onDrop={(x: unknown) => {}} - dropType="field_add" + dropTypes={['field_add']} getAdditionalClassesOnEnter={getAdditionalClasses} getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} > @@ -300,219 +309,652 @@ describe('DragDrop', () => { .find('[data-test-subj="lnsDragDrop"]') .first() .simulate('dragstart', { dataTransfer }); - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); - expect(component.find('.additional')).toHaveLength(1); + expect(component.find('.additional')).toHaveLength(2); component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); expect(setActiveDropTarget).toBeCalledWith(undefined); }); - test('Keyboard navigation: User receives proper drop Targets highlighted when pressing arrow keys', () => { - const onDrop = jest.fn(); - const setActiveDropTarget = jest.fn(); - const setA11yMessage = jest.fn(); - const items = [ - { - draggable: true, - value: { - id: '1', - humanData: { label: 'Label1', position: 1 }, + describe('Keyboard navigation', () => { + test('User receives proper drop Targets highlighted when pressing arrow keys', () => { + const onDrop = jest.fn(); + const setActiveDropTarget = jest.fn(); + const setA11yMessage = jest.fn(); + const items = [ + { + draggable: true, + value: { + id: '1', + humanData: { label: 'Label1', position: 1 }, + }, + children: '1', + order: [2, 0, 0, 0], }, - children: '1', - order: [2, 0, 0, 0], - }, - { - draggable: true, - dragType: 'move' as 'copy' | 'move', + { + draggable: true, + dragType: 'move' as 'copy' | 'move', - value: { - id: '2', + value: { + id: '2', - humanData: { label: 'label2', position: 1 }, - }, - onDrop, - dropType: 'move_compatible' as DropType, - order: [2, 0, 1, 0], - }, - { - draggable: true, - dragType: 'move' as 'copy' | 'move', - value: { - id: '3', - humanData: { label: 'label3', position: 1, groupLabel: 'Y' }, + humanData: { label: 'label2', position: 1 }, + }, + onDrop, + dropTypes: ['move_compatible'] as DropType[], + order: [2, 0, 1, 0], }, - onDrop, - dropType: 'replace_compatible' as DropType, - order: [2, 0, 2, 0], - }, - { - draggable: true, - dragType: 'move' as 'copy' | 'move', - value: { - id: '4', - humanData: { label: 'label4', position: 2, groupLabel: 'Y' }, + { + draggable: true, + dragType: 'move' as 'copy' | 'move', + value: { + id: '3', + humanData: { + label: 'label3', + position: 1, + groupLabel: 'Y', + canSwap: true, + canDuplicate: true, + }, + }, + onDrop, + dropTypes: [ + 'replace_compatible', + 'duplicate_compatible', + 'swap_compatible', + ] as DropType[], + order: [2, 0, 2, 0], }, - order: [2, 0, 2, 1], - }, - ]; - const component = mount( - , style: {} } }, - setActiveDropTarget, - setA11yMessage, - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, + { + draggable: true, + dragType: 'move' as 'copy' | 'move', + value: { + id: '4', + humanData: { label: 'label4', position: 2, groupLabel: 'Y' }, }, - keyboardMode: true, - }} - > - {items.map((props) => ( - -
- - ))} - - ); - const keyboardHandler = component - .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') - .first() - .simulate('focus'); - act(() => { + order: [2, 0, 2, 1], + }, + ]; + const component = mount( + , style: {} } }, + setActiveDropTarget, + setA11yMessage, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + '2,0,2,0,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, + '2,0,1,0,1': { ...items[1].value, onDrop, dropType: 'duplicate_compatible' }, + '2,0,1,0,2': { ...items[1].value, onDrop, dropType: 'swap_compatible' }, + }, + keyboardMode: true, + }} + > + {items.map((props) => ( + +
+ + ))} + + ); + const keyboardHandler = component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('focus'); + keyboardHandler.simulate('keydown', { key: 'ArrowRight' }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[2].value, + onDrop, + dropType: items[2].dropTypes![0], + }); + keyboardHandler.simulate('keydown', { key: 'Enter' }); + expect(setA11yMessage).toBeCalledWith( + `You're dragging Label1 from at position 1 over label3 from Y group at position 1. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.` + ); + expect(setActiveDropTarget).toBeCalledWith(undefined); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'Label1', position: 1 }, id: '1' }, + 'move_compatible' + ); }); - expect(setActiveDropTarget).toBeCalledWith({ - ...items[2].value, - onDrop, - dropType: items[2].dropType, + + test('dragstart sets dragging in the context and calls it with proper params', async () => { + const setDragging = jest.fn(); + + const setA11yMessage = jest.fn(); + const component = mount( + + + + + + ); + + const keyboardHandler = component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('focus'); + + keyboardHandler.simulate('keydown', { key: 'Enter' }); + act(() => { + jest.runAllTimers(); + }); + + expect(setDragging).toBeCalledWith({ + ...value, + ghost: { + children: , + style: { + height: 0, + width: 0, + }, + }, + }); + expect(setA11yMessage).toBeCalledWith('Lifted hello'); }); - keyboardHandler.simulate('keydown', { key: 'Enter' }); - expect(setA11yMessage).toBeCalledWith( - 'Replace label3 in Y group at position 1 with Label1. Press space or enter to replace' - ); - expect(setActiveDropTarget).toBeCalledWith(undefined); - expect(onDrop).toBeCalledWith( - { humanData: { label: 'Label1', position: 1 }, id: '1' }, - 'move_compatible' - ); - }); - test('Keyboard navigation: dragstart sets dragging in the context and calls it with proper params', async () => { - const setDragging = jest.fn(); + test('ActiveDropTarget gets ghost image', () => { + const onDrop = jest.fn(); + const setActiveDropTarget = jest.fn(); + const setA11yMessage = jest.fn(); + const items = [ + { + draggable: true, + value: { + id: '1', + humanData: { label: 'Label1', position: 1 }, + }, + children: '1', + order: [2, 0, 0, 0], + }, + { + draggable: true, + dragType: 'move' as 'copy' | 'move', - const setA11yMessage = jest.fn(); - const component = mount( - - - - - - ); + value: { + id: '2', - const keyboardHandler = component - .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') - .first() - .simulate('focus'); - - keyboardHandler.simulate('keydown', { key: 'Enter' }); - jest.runAllTimers(); - - expect(setDragging).toBeCalledWith({ - ...value, - ghost: { - children: , - style: { - height: 0, - width: 0, + humanData: { label: 'label2', position: 1 }, + }, + onDrop, + dropTypes: ['move_compatible'] as DropType[], + order: [2, 0, 1, 0], }, - }, + ]; + const component = mount( + Hello
, style: {} } }, + setActiveDropTarget, + setA11yMessage, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + }, + keyboardMode: true, + }} + > + {items.map((props) => ( + +
+ + ))} + + ); + + expect(component.find(DragDrop).at(1).find('.lnsDragDrop_ghost').text()).toEqual('Hello'); }); - expect(setA11yMessage).toBeCalledWith('Lifted hello'); }); - test('Keyboard navigation: ActiveDropTarget gets ghost image', () => { + describe('multiple drop targets', () => { + let activeDropTarget: DragContextState['activeDropTarget']; const onDrop = jest.fn(); - const setActiveDropTarget = jest.fn(); + let setActiveDropTarget = jest.fn(); const setA11yMessage = jest.fn(); - const items = [ - { - draggable: true, - value: { - id: '1', - humanData: { label: 'Label1', position: 1 }, - }, - children: '1', - order: [2, 0, 0, 0], - }, - { - draggable: true, - dragType: 'move' as 'copy' | 'move', + let component: ReactWrapper; + beforeEach(() => { + activeDropTarget = undefined; + setActiveDropTarget = jest.fn((val) => { + activeDropTarget = value as DragContextState['activeDropTarget']; + }); + component = mount( + true} + dropTargetsByOrder={undefined} + registerDropTarget={jest.fn()} + > + + + +
{dropType}
} + > + +
+
+ ); + }); + test('extra drop targets render correctly', () => { + expect(component.find('.extraDrop').hostNodes()).toHaveLength(2); + }); - value: { - id: '2', + test('extra drop targets appear when dragging over and disappear when activeDropTarget changes', () => { + component.find('[data-test-subj="lnsDragDropContainer"]').first().simulate('dragenter'); - humanData: { label: 'label2', position: 1 }, - }, + // customDropTargets are visible + expect(component.find('[data-test-subj="lnsDragDropContainer"]').prop('className')).toEqual( + 'lnsDragDrop__container lnsDragDrop__container-active' + ); + expect( + component.find('[data-test-subj="lnsDragDropExtraDrops"]').first().prop('className') + ).toEqual('lnsDragDrop__extraDrops lnsDragDrop__extraDrops-visible'); + + // set activeDropTarget as undefined + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); + act(() => { + jest.runAllTimers(); + }); + component.update(); + + // customDropTargets are invisible + expect( + component.find('[data-test-subj="lnsDragDropExtraDrops"]').first().prop('className') + ).toEqual('lnsDragDrop__extraDrops'); + }); + + test('dragging over different drop types of the same value assigns correct activeDropTarget', () => { + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + + component.find('SingleDropInner').at(0).simulate('dragover'); + + expect(setActiveDropTarget).toBeCalledWith({ + ...value, + dropType: 'move_compatible', onDrop, - dropType: 'move_compatible' as DropType, - order: [2, 0, 1, 0], - }, - ]; - const component = mount( - Hello
, style: {} } }, - setActiveDropTarget, - setA11yMessage, - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + }); + + component.find('SingleDropInner').at(1).simulate('dragover'); + + expect(setActiveDropTarget).toBeCalledWith({ + ...value, + dropType: 'duplicate_compatible', + onDrop, + }); + + component.find('SingleDropInner').at(2).simulate('dragover'); + expect(setActiveDropTarget).toBeCalledWith({ + ...value, + dropType: 'swap_compatible', + onDrop, + }); + component.find('SingleDropInner').at(2).simulate('dragleave'); + expect(setActiveDropTarget).toBeCalledWith(undefined); + }); + + test('drop on extra drop target passes correct dropType to onDrop', () => { + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + + component.find('SingleDropInner').at(0).simulate('dragover'); + component.find('SingleDropInner').at(0).simulate('drop'); + expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'move_compatible'); + + component.find('SingleDropInner').at(1).simulate('dragover'); + component.find('SingleDropInner').at(1).simulate('drop'); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'Label1' }, id: '1' }, + 'duplicate_compatible' + ); + + component.find('SingleDropInner').at(2).simulate('dragover'); + component.find('SingleDropInner').at(2).simulate('drop'); + expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'swap_compatible'); + }); + + test('pressing Alt or Shift when dragging over the main drop target sets extra drop target as active', () => { + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + + // needed to setup activeDropType + component + .find('SingleDropInner') + .at(0) + .simulate('dragover', { altKey: true }) + .simulate('dragover', { altKey: true }); + expect(setActiveDropTarget).toBeCalledWith({ + ...value, + dropType: 'duplicate_compatible', + onDrop, + }); + + component + .find('SingleDropInner') + .at(0) + .simulate('dragover', { shiftKey: true }) + .simulate('dragover', { shiftKey: true }); + expect(setActiveDropTarget).toBeCalledWith({ + ...value, + dropType: 'swap_compatible', + onDrop, + }); + }); + + test('pressing Alt or Shift when dragging over the extra drop target does nothing', () => { + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + + const extraDrop = component.find('SingleDropInner').at(1); + extraDrop.simulate('dragover', { altKey: true }); + extraDrop.simulate('dragover', { shiftKey: true }); + extraDrop.simulate('dragover'); + expect( + setActiveDropTarget.mock.calls.every((call) => call[0].dropType === 'duplicate_compatible') + ); + }); + describe('keyboard navigation', () => { + const items = [ + { + draggable: true, + value: { + id: '1', + humanData: { label: 'Label1', position: 1 }, }, - keyboardMode: true, - }} - > - {items.map((props) => ( - -
- - ))} - - ); + children: '1', + order: [2, 0, 0, 0], + }, + { + draggable: true, + dragType: 'move' as const, - expect(component.find(DragDrop).at(1).find('.lnsDragDrop_ghost').text()).toEqual('Hello'); + value: { + id: '2', + + humanData: { label: 'label2', position: 1 }, + }, + onDrop, + dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'] as DropType[], + order: [2, 0, 1, 0], + }, + { + draggable: true, + dragType: 'move' as const, + value: { + id: '3', + humanData: { label: 'label3', position: 1, groupLabel: 'Y' }, + }, + onDrop, + dropTypes: ['replace_compatible'] as DropType[], + order: [2, 0, 2, 0], + }, + ]; + const assignedDropTargetsByOrder: DragContextState['dropTargetsByOrder'] = { + '2,0,1,0,0': { + ...items[1].value, + onDrop, + dropType: 'move_compatible', + }, + '2,0,1,0,1': { + dropType: 'duplicate_compatible', + humanData: { + label: 'label2', + position: 1, + }, + id: '2', + onDrop, + }, + '2,0,1,0,2': { + dropType: 'swap_compatible', + humanData: { + label: 'label2', + position: 1, + }, + id: '2', + onDrop, + }, + '2,0,2,0,0': { + dropType: 'replace_compatible', + humanData: { + groupLabel: 'Y', + label: 'label3', + position: 1, + }, + id: '3', + onDrop, + }, + }; + test('when pressing enter key, context receives the proper dropTargetsByOrder', () => { + let dropTargetsByOrder: DragContextState['dropTargetsByOrder'] = {}; + const setKeyboardMode = jest.fn(); + component = mount( + , style: {} } }, + setDragging: jest.fn(), + setActiveDropTarget, + setA11yMessage, + activeDropTarget, + dropTargetsByOrder, + keyboardMode: true, + setKeyboardMode, + registerDropTarget: jest.fn((order, dropTarget) => { + dropTargetsByOrder = { + ...dropTargetsByOrder, + [order.join(',')]: dropTarget, + }; + }), + }} + > + {items.map((props) => ( + +
+ + ))} + + ); + component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]').first().simulate('focus'); + act(() => { + jest.runAllTimers(); + }); + component.update(); + expect(dropTargetsByOrder).toEqual(assignedDropTargetsByOrder); + }); + test('when pressing ArrowRight key with modifier key pressed in, the extra drop target is selected', () => { + component = mount( + , style: {} } }, + setDragging: jest.fn(), + setActiveDropTarget, + setA11yMessage, + activeDropTarget: undefined, + dropTargetsByOrder: assignedDropTargetsByOrder, + keyboardMode: true, + setKeyboardMode: jest.fn(), + registerDropTarget: jest.fn(), + }} + > + {items.map((props) => ( + +
+ + ))} + + ); + act(() => { + component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('keydown', { key: 'ArrowRight', altKey: true }); + }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[1].value, + onDrop, + dropType: 'duplicate_compatible', + }); + act(() => { + component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('keydown', { key: 'ArrowRight', shiftKey: true }); + }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[1].value, + onDrop, + dropType: 'swap_compatible', + }); + }); + test('when having a main target selected and pressing alt, the first extra drop target is selected', () => { + component = mount( + , style: {} } }, + setDragging: jest.fn(), + setActiveDropTarget, + setA11yMessage, + activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'], + dropTargetsByOrder: assignedDropTargetsByOrder, + keyboardMode: true, + setKeyboardMode: jest.fn(), + registerDropTarget: jest.fn(), + }} + > + {items.map((props) => ( + +
+ + ))} + + ); + act(() => { + component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('keydown', { key: 'Alt' }); + }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[1].value, + onDrop, + dropType: 'duplicate_compatible', + }); + act(() => { + component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('keyup', { key: 'Alt' }); + }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[1].value, + onDrop, + dropType: 'move_compatible', + }); + }); + test('when having a main target selected and pressing shift, the second extra drop target is selected', () => { + component = mount( + , style: {} } }, + setDragging: jest.fn(), + setActiveDropTarget, + setA11yMessage, + activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'], + dropTargetsByOrder: assignedDropTargetsByOrder, + keyboardMode: true, + setKeyboardMode: jest.fn(), + registerDropTarget: jest.fn(), + }} + > + {items.map((props) => ( + +
+ + ))} + + ); + act(() => { + component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('keydown', { key: 'Shift' }); + }); + + expect(setActiveDropTarget).toBeCalledWith({ + ...items[1].value, + onDrop, + dropType: 'swap_compatible', + }); + act(() => { + component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('keyup', { key: 'Shift' }); + }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[1].value, + onDrop, + dropType: 'move_compatible', + }); + }); + }); }); - describe('reordering', () => { + describe('Reordering', () => { const onDrop = jest.fn(); const items = [ { id: '1', humanData: { label: 'Label1', position: 1, groupLabel: 'X' }, onDrop, - dropType: 'reorder' as DropType, + draggable: true, }, { id: '2', humanData: { label: 'label2', position: 2, groupLabel: 'X' }, onDrop, - dropType: 'reorder' as DropType, }, { id: '3', humanData: { label: 'label3', position: 3, groupLabel: 'X' }, onDrop, - dropType: 'reorder' as DropType, }, ]; const mountComponent = ( @@ -546,7 +988,6 @@ describe('DragDrop', () => { const dragDropSharedProps = { draggable: true, dragType: 'move' as 'copy' | 'move', - dropType: 'reorder' as DropType, reorderableGroup: items.map(({ id }) => ({ id })), onDrop: onDropHandler || onDrop, }; @@ -557,15 +998,25 @@ describe('DragDrop', () => { 1 - + 2 - + 3 @@ -574,7 +1025,10 @@ describe('DragDrop', () => { }; test(`Inactive group renders properly`, () => { const component = mountComponent(undefined); - expect(component.find('[data-test-subj="lnsDragDrop"]')).toHaveLength(3); + act(() => { + jest.runAllTimers(); + }); + expect(component.find('[data-test-subj="lnsDragDrop"]')).toHaveLength(5); }); test(`Reorderable group with lifted element renders properly`, () => { @@ -585,31 +1039,32 @@ describe('DragDrop', () => { setDragging, setA11yMessage, }); + + act(() => { + jest.runAllTimers(); + }); + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + act(() => { - component - .find('[data-test-subj="lnsDragDrop"]') - .first() - .simulate('dragstart', { dataTransfer }); jest.runAllTimers(); }); expect(setDragging).toBeCalledWith({ ...items[0] }); expect(setA11yMessage).toBeCalledWith('Lifted Label1'); - expect( - component - .find('[data-test-subj="lnsDragDrop-reorderableGroup"]') - .hasClass('lnsDragDrop-isActiveGroup') - ).toEqual(true); }); test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => { const component = mountComponent({ dragging: { ...items[0] } }); + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + act(() => { - component - .find('[data-test-subj="lnsDragDrop"]') - .first() - .simulate('dragstart', { dataTransfer }); jest.runAllTimers(); }); @@ -656,14 +1111,16 @@ describe('DragDrop', () => { setA11yMessage, }); - component - .find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]') - .at(1) - .simulate('drop', { preventDefault, stopPropagation }); - jest.runAllTimers(); + const dragDrop = component.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]').at(1); + dragDrop.simulate('dragOver'); + dragDrop.simulate('drop', { preventDefault, stopPropagation }); + + act(() => { + jest.runAllTimers(); + }); expect(setA11yMessage).toBeCalledWith( - 'Reordered Label1 in X group from position 1 to positon 3' + 'Reordered Label1 in X group from position 1 to position 3' ); expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); @@ -685,7 +1142,9 @@ describe('DragDrop', () => { setActiveDropTarget, setA11yMessage, }); - const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + const keyboardHandler = component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first(); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); @@ -694,11 +1153,12 @@ describe('DragDrop', () => { keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); - expect(setActiveDropTarget).toBeCalledWith(items[1]); + expect(setActiveDropTarget).toBeCalledWith({ ...items[1], dropType: 'reorder' }); expect(setA11yMessage).toBeCalledWith( 'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder' ); }); + test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { const component = mountComponent({ dragging: { ...items[0] }, @@ -712,6 +1172,7 @@ describe('DragDrop', () => { }); const keyboardHandler = component .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() .simulate('focus'); act(() => { @@ -732,7 +1193,9 @@ describe('DragDrop', () => { const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'Escape' }); - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); expect(onDropHandler).not.toHaveBeenCalled(); expect(setA11yMessage).toBeCalledWith( @@ -828,7 +1291,7 @@ describe('DragDrop', () => { ; +const noop = () => {}; + /** * The base props to the DragDrop component. */ @@ -53,7 +57,7 @@ interface BaseProps { /** * The React element which will be passed the draggable handlers */ - children: React.ReactElement; + children: ReactElement; /** * Indicates whether or not this component is draggable. */ @@ -85,14 +89,18 @@ interface BaseProps { dragType?: 'copy' | 'move'; /** - * Indicates the type of a drop - when undefined, the currently dragged item + * Indicates the type of drop targets - when undefined, the currently dragged item * cannot be dropped onto this component. */ - dropType?: DropType; + dropTypes?: DropType[]; /** * Order for keyboard dragging. This takes an array of numbers which will be used to order hierarchically */ order: number[]; + /** + * Extra drop targets by dropType + */ + getCustomDropTarget?: (dropType: DropType) => ReactElement | null; } /** @@ -109,19 +117,17 @@ interface DragInnerProps extends BaseProps { dropTargetsByOrder: DragContextState['dropTargetsByOrder']; }; onDragStart?: ( - target?: - | DroppableEvent['currentTarget'] - | React.KeyboardEvent['currentTarget'] + target?: DroppableEvent['currentTarget'] | KeyboardEvent['currentTarget'] ) => void; onDragEnd?: () => void; - extraKeyboardHandler?: (e: React.KeyboardEvent) => void; + extraKeyboardHandler?: (e: KeyboardEvent) => void; ariaDescribedBy?: string; } /** * The props for a non-draggable instance of that component. */ -interface DropInnerProps extends BaseProps { +interface DropsInnerProps extends BaseProps { dragging: DragContextState['dragging']; keyboardMode: DragContextState['keyboardMode']; setKeyboardMode: DragContextState['setKeyboardMode']; @@ -129,7 +135,7 @@ interface DropInnerProps extends BaseProps { setActiveDropTarget: DragContextState['setActiveDropTarget']; setA11yMessage: DragContextState['setA11yMessage']; registerDropTarget: DragContextState['registerDropTarget']; - isActiveDropTarget: boolean; + activeDropTarget: DragContextState['activeDropTarget']; isNotDroppable: boolean; } @@ -148,7 +154,7 @@ export const DragDrop = (props: BaseProps) => { setA11yMessage, } = useContext(DragContext); - const { value, draggable, dropType, reorderableGroup } = props; + const { value, draggable, dropTypes, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); const activeDraggingProps = isDragging @@ -159,7 +165,7 @@ export const DragDrop = (props: BaseProps) => { } : undefined; - if (draggable && !dropType) { + if (draggable && (!dropTypes || !dropTypes.length)) { const dragProps = { ...props, activeDraggingProps, @@ -175,14 +181,13 @@ export const DragDrop = (props: BaseProps) => { } } - const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id); const dropProps = { ...props, keyboardMode, setKeyboardMode, dragging, setDragging, - isActiveDropTarget, + activeDropTarget, setActiveDropTarget, registerDropTarget, setA11yMessage, @@ -190,19 +195,20 @@ export const DragDrop = (props: BaseProps) => { // If the configuration has provided a droppable flag, but this particular item is not // droppable, then it should be less prominent. Ignores items that are both // draggable and drop targets - !!(!dropType && dragging && value.id !== dragging.id), + !!((!dropTypes || !dropTypes.length) && dragging && value.id !== dragging.id), }; if ( reorderableGroup && reorderableGroup.length > 1 && - reorderableGroup?.some((i) => i.id === dragging?.id) + reorderableGroup?.some((i) => i.id === dragging?.id) && + dropTypes?.[0] === 'reorder' ) { return ; } - return ; + return ; }; -const removeSelectionBeforeDragging = () => { +const removeSelection = () => { const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); @@ -230,8 +236,60 @@ const DragInner = memo(function DragInner({ const activeDropTarget = activeDraggingProps?.activeDropTarget; const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const setTarget = useCallback( + (target?: DropIdentifier, announceModifierKeys = false) => { + setActiveDropTarget(target); + setA11yMessage( + target + ? announce.selectedTarget( + value.humanData, + target?.humanData, + target?.dropType, + announceModifierKeys + ) + : announce.noTarget() + ); + }, + [setActiveDropTarget, setA11yMessage, value.humanData] + ); + + const setTargetOfIndex = useCallback( + (id: string, index: number) => { + const dropTargetsForActiveId = + dropTargetsByOrder && + Object.values(dropTargetsByOrder).filter((dropTarget) => dropTarget?.id === id); + if (index > 0 && dropTargetsForActiveId?.[index]) { + setTarget(dropTargetsForActiveId[index]); + } else { + setTarget(dropTargetsForActiveId?.[0], true); + } + }, + [dropTargetsByOrder, setTarget] + ); + const modifierHandlers = useMemo(() => { + const onKeyUp = (e: KeyboardEvent) => { + if ((e.key === 'Shift' || e.key === 'Alt') && activeDropTarget?.id) { + if (e.altKey) { + setTargetOfIndex(activeDropTarget.id, 1); + } else if (e.shiftKey) { + setTargetOfIndex(activeDropTarget.id, 2); + } else { + setTargetOfIndex(activeDropTarget.id, 0); + } + } + }; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Alt' && activeDropTarget?.id) { + setTargetOfIndex(activeDropTarget.id, 1); + } else if (e.key === 'Shift' && activeDropTarget?.id) { + setTargetOfIndex(activeDropTarget.id, 2); + } + }; + return { onKeyDown, onKeyUp }; + }, [activeDropTarget, setTargetOfIndex]); + const dragStart = ( - e: DroppableEvent | React.KeyboardEvent, + e: DroppableEvent | KeyboardEvent, keyboardModeOn?: boolean ) => { // Setting stopPropgagation causes Chrome failures, so @@ -282,20 +340,8 @@ const DragInner = memo(function DragInner({ onDragEnd(); } }; - const dropToActiveDropTarget = () => { - if (activeDropTarget) { - trackUiEvent('drop_total'); - const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; - setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); - onTargetDrop(value, dropType); - } - }; - - const setNextTarget = (reversed = false) => { - if (!order) { - return; - } + const setNextTarget = (e: KeyboardEvent, reversed = false) => { const nextTarget = nextValidDropTarget( dropTargetsByOrder, activeDropTarget, @@ -304,13 +350,24 @@ const DragInner = memo(function DragInner({ reversed ); - setActiveDropTarget(nextTarget); - setA11yMessage( - nextTarget - ? announce.selectedTarget(value.humanData, nextTarget?.humanData, nextTarget?.dropType) - : announce.noTarget() - ); + if (e.altKey && nextTarget?.id) { + setTargetOfIndex(nextTarget.id, 1); + } else if (e.shiftKey && nextTarget?.id) { + setTargetOfIndex(nextTarget.id, 2); + } else { + setTarget(nextTarget, true); + } }; + + const dropToActiveDropTarget = () => { + if (activeDropTarget) { + trackUiEvent('drop_total'); + const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; + setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); + onTargetDrop(value, dropType); + } + }; + const shouldShowGhostImageInstead = dragType === 'move' && keyboardMode && @@ -319,7 +376,9 @@ const DragInner = memo(function DragInner({ return (
@@ -334,7 +393,7 @@ const DragInner = memo(function DragInner({ dragEnd(); } }} - onKeyDown={(e: React.KeyboardEvent) => { + onKeyDown={(e: KeyboardEvent) => { const { key } = e; if (key === keys.ENTER || key === keys.SPACE) { if (activeDropTarget) { @@ -356,30 +415,30 @@ const DragInner = memo(function DragInner({ if (extraKeyboardHandler) { extraKeyboardHandler(e); } - if (keyboardMode && (keys.ARROW_LEFT === key || keys.ARROW_RIGHT === key)) { - setNextTarget(!!(keys.ARROW_LEFT === key)); + if (keyboardMode) { + if (keys.ARROW_LEFT === key || keys.ARROW_RIGHT === key) { + setNextTarget(e, !!(keys.ARROW_LEFT === key)); + } + modifierHandlers.onKeyDown(e); } }} + onKeyUp={modifierHandlers.onKeyUp} /> {React.cloneElement(children, { 'data-test-subj': dataTestSubj || 'lnsDragDrop', - className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', { - 'lnsDragDrop-isHidden': - (activeDraggingProps && dragType === 'move' && !keyboardMode) || - shouldShowGhostImageInstead, - }), + className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable'), draggable: true, onDragEnd: dragEnd, onDragStart: dragStart, - onMouseDown: removeSelectionBeforeDragging, + onMouseDown: removeSelection, })}
); }); -const DropInner = memo(function DropInner(props: DropInnerProps) { +const DropsInner = memo(function DropsInner(props: DropsInnerProps) { const { dataTestSubj, className, @@ -389,54 +448,86 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { draggable, dragging, isNotDroppable, - dropType, + dropTypes, order, getAdditionalClassesOnEnter, getAdditionalClassesOnDroppable, - isActiveDropTarget, + activeDropTarget, registerDropTarget, setActiveDropTarget, keyboardMode, setKeyboardMode, setDragging, setA11yMessage, + getCustomDropTarget, } = props; + const [isInZone, setIsInZone] = useState(false); + const mainTargetRef = useRef(null); + useShallowCompareEffect(() => { - if (dropType && onDrop && keyboardMode) { - registerDropTarget(order, { ...value, onDrop, dropType }); + if (dropTypes && dropTypes?.[0] && onDrop && keyboardMode) { + dropTypes.forEach((dropType, index) => { + registerDropTarget([...order, index], { ...value, onDrop, dropType }); + }); return () => { - registerDropTarget(order, undefined); + dropTypes.forEach((_, index) => { + registerDropTarget([...order, index], undefined); + }); }; } - }, [order, value, registerDropTarget, dropType, keyboardMode]); - - const classesOnEnter = getAdditionalClassesOnEnter?.(dropType); - const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); - - const classes = classNames( - 'lnsDragDrop', - { - 'lnsDragDrop-isDraggable': draggable, - 'lnsDragDrop-isDroppable': !draggable, - 'lnsDragDrop-isDropTarget': dropType && dropType !== 'reorder', - 'lnsDragDrop-isActiveDropTarget': dropType && isActiveDropTarget && dropType !== 'reorder', - 'lnsDragDrop-isNotDroppable': isNotDroppable, - }, - classesOnEnter && { [classesOnEnter]: isActiveDropTarget }, - classesOnDroppable && { [classesOnDroppable]: dropType } - ); + }, [order, registerDropTarget, dropTypes, keyboardMode]); - const dragOver = (e: DroppableEvent) => { - if (!dropType) { - return; + useEffect(() => { + if (activeDropTarget && activeDropTarget.id !== value.id) { + setIsInZone(false); } + setTimeout(() => { + if (!activeDropTarget) { + setIsInZone(false); + } + }, 1000); + }, [activeDropTarget, setIsInZone, value.id]); + + const dragEnter = () => { + if (!isInZone) { + setIsInZone(true); + } + }; + + const getModifiedDropType = (e: DroppableEvent, dropType: DropType) => { + if (!dropTypes || dropTypes.length <= 1) { + return dropType; + } + const dropIndex = dropTypes.indexOf(dropType); + if (dropIndex > 0) { + return dropType; + } else if (dropIndex === 0) { + if (e.altKey && dropTypes[1]) { + return dropTypes[1]; + } else if (e.shiftKey && dropTypes[2]) { + return dropTypes[2]; + } + } + return dropType; + }; + + const dragOver = (e: DroppableEvent, dropType: DropType) => { e.preventDefault(); + if (!dragging || !onDrop) { + return; + } + const modifiedDropType = getModifiedDropType(e, dropType); + const isActiveDropTarget = !!( + activeDropTarget?.id === value.id && activeDropTarget?.dropType === modifiedDropType + ); // An optimization to prevent a bunch of React churn. - if (!isActiveDropTarget && dragging && onDrop) { - setActiveDropTarget({ ...value, dropType, onDrop }); - setA11yMessage(announce.selectedTarget(dragging.humanData, value.humanData, dropType)); + if (!isActiveDropTarget) { + setActiveDropTarget({ ...value, dropType: modifiedDropType, onDrop }); + setA11yMessage( + announce.selectedTarget(dragging.humanData, value.humanData, modifiedDropType) + ); } }; @@ -444,35 +535,146 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { setActiveDropTarget(undefined); }; - const drop = (e: DroppableEvent | React.KeyboardEvent) => { + const drop = (e: DroppableEvent, dropType: DropType) => { e.preventDefault(); e.stopPropagation(); - - if (onDrop && dropType && dragging) { - trackUiEvent('drop_total'); - onDrop(dragging, dropType); + setIsInZone(false); + if (onDrop && dragging) { + const modifiedDropType = getModifiedDropType(e, dropType); + onDrop(dragging, modifiedDropType); setTimeout(() => - setA11yMessage(announce.dropped(dragging.humanData, value.humanData, dropType)) + setA11yMessage(announce.dropped(dragging.humanData, value.humanData, modifiedDropType)) ); } + setDragging(undefined); setActiveDropTarget(undefined); setKeyboardMode(false); }; - const ghost = - isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost ? dragging.ghost : undefined; + const getProps = (dropType?: DropType, dropChildren?: ReactElement) => { + const isActiveDropTarget = Boolean( + activeDropTarget?.id === value.id && dropType === activeDropTarget?.dropType + ); + return { + 'data-test-subj': dataTestSubj || 'lnsDragDrop', + className: getClasses(dropType, dropChildren), + onDragEnter: dragEnter, + onDragLeave: dragLeave, + onDragOver: dropType ? (e: DroppableEvent) => dragOver(e, dropType) : noop, + onDrop: dropType ? (e: DroppableEvent) => drop(e, dropType) : noop, + draggable, + ghost: + (isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost && dragging.ghost) || + undefined, + }; + }; + + const getClasses = (dropType?: DropType, dropChildren = children) => { + const isActiveDropTarget = Boolean( + activeDropTarget?.id === value.id && dropType === activeDropTarget?.dropType + ); + const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); + + const classes = classNames( + 'lnsDragDrop', + { + 'lnsDragDrop-isDraggable': draggable, + 'lnsDragDrop-isDroppable': !draggable, + 'lnsDragDrop-isDropTarget': dropType, + 'lnsDragDrop-isActiveDropTarget': dropType && isActiveDropTarget, + 'lnsDragDrop-isNotDroppable': isNotDroppable, + }, + classesOnDroppable && { [classesOnDroppable]: dropType } + ); + return classNames(classes, className, dropChildren.props.className); + }; + + const getMainTargetClasses = () => { + const classesOnEnter = getAdditionalClassesOnEnter?.(activeDropTarget?.dropType); + return classNames(classesOnEnter && { [classesOnEnter]: activeDropTarget?.id === value.id }); + }; + + const mainTargetProps = getProps(dropTypes && dropTypes[0]); + + const extraDropStyles = useMemo(() => { + const extraDrops = dropTypes && dropTypes.length && dropTypes.slice(1); + if (!extraDrops || !extraDrops.length) { + return; + } + + const height = extraDrops.length * 40; + const minHeight = height - (mainTargetRef.current?.clientHeight || 40); + const clipPath = `polygon(100% 0px, 100% ${height - minHeight}px, 0 100%, 0 0)`; + return { + clipPath, + height, + }; + }, [dropTypes]); + + return ( +
+ + {dropTypes && dropTypes.length > 1 && ( + <> +
+ + {dropTypes.slice(1).map((dropType) => { + const dropChildren = getCustomDropTarget?.(dropType); + return dropChildren ? ( + + + {dropChildren} + + + ) : null; + })} + + + )} +
+ ); +}); +const SingleDropInner = ({ + ghost, + children, + ...rest +}: { + ghost?: Ghost; + children: ReactElement; + style?: React.CSSProperties; + className?: string; +}) => { return ( <> - {React.cloneElement(children, { - 'data-test-subj': dataTestSubj || 'lnsDragDrop', - className: classNames(children.props.className, classes, className), - onDragOver: dragOver, - onDragLeave: dragLeave, - onDrop: drop, - draggable, - })} + {React.cloneElement(children, rest)} {ghost ? React.cloneElement(ghost.children, { className: classNames(ghost.children.props.className, 'lnsDragDrop_ghost'), @@ -481,7 +683,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { : null} ); -}); +}; const ReorderableDrag = memo(function ReorderableDrag( props: DragInnerProps & { reorderableGroup: Array<{ id: string }>; dragging?: DragDropIdentifier } @@ -519,7 +721,7 @@ const ReorderableDrag = memo(function ReorderableDrag( const onReorderableDragStart = ( currentTarget?: | DroppableEvent['currentTarget'] - | React.KeyboardEvent['currentTarget'] + | KeyboardEvent['currentTarget'] ) => { if (currentTarget) { const height = currentTarget.offsetHeight + lnsLayerPanelDimensionMargin; @@ -540,7 +742,7 @@ const ReorderableDrag = memo(function ReorderableDrag( reorderedItems: [], })); - const extraKeyboardHandler = (e: React.KeyboardEvent) => { + const extraKeyboardHandler = (e: KeyboardEvent) => { if (isReorderOn && keyboardMode) { e.stopPropagation(); e.preventDefault(); @@ -644,7 +846,7 @@ const ReorderableDrag = memo(function ReorderableDrag( }); const ReorderableDrop = memo(function ReorderableDrop( - props: DropInnerProps & { reorderableGroup: Array<{ id: string }> } + props: DropsInnerProps & { reorderableGroup: Array<{ id: string }> } ) { const { onDrop, @@ -652,11 +854,10 @@ const ReorderableDrop = memo(function ReorderableDrop( dragging, setDragging, setKeyboardMode, - isActiveDropTarget, + activeDropTarget, setActiveDropTarget, reorderableGroup, setA11yMessage, - dropType, } = props; const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id); @@ -666,7 +867,7 @@ const ReorderableDrop = memo(function ReorderableDrop( setReorderState, } = useContext(ReorderContext); - const heightRef = React.useRef(null); + const heightRef = useRef(null); const isReordered = isReorderOn && reorderedItems.some((el) => el.id === value.id) && reorderedItems.length; @@ -688,42 +889,38 @@ const ReorderableDrop = memo(function ReorderableDrop( }, [isReordered, setReorderState, value.id]); const onReorderableDragOver = (e: DroppableEvent) => { - if (!dropType) { - return; - } e.preventDefault(); - // An optimization to prevent a bunch of React churn. - if (!isActiveDropTarget && dropType && onDrop) { - setActiveDropTarget({ ...value, dropType, onDrop }); - } + if (activeDropTarget?.id !== value?.id && onDrop) { + setActiveDropTarget({ ...value, dropType: 'reorder', onDrop }); - const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id); + const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id); - if (!dragging || draggingIndex === -1) { - return; - } - const droppingIndex = currentIndex; - if (draggingIndex === droppingIndex) { - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); - } + if (!dragging || draggingIndex === -1) { + return; + } + const droppingIndex = currentIndex; + if (draggingIndex === droppingIndex) { + setReorderState((s: ReorderState) => ({ + ...s, + reorderedItems: [], + })); + } - setReorderState((s: ReorderState) => - draggingIndex < droppingIndex - ? { - ...s, - reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1), - direction: '-', - } - : { - ...s, - reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex), - direction: '+', - } - ); + setReorderState((s: ReorderState) => + draggingIndex < droppingIndex + ? { + ...s, + reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1), + direction: '-', + } + : { + ...s, + reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex), + direction: '+', + } + ); + } }; const onReorderableDrop = (e: DroppableEvent) => { @@ -734,7 +931,7 @@ const ReorderableDrop = memo(function ReorderableDrop( setDragging(undefined); setKeyboardMode(false); - if (onDrop && dropType && dragging) { + if (onDrop && dragging) { trackUiEvent('drop_total'); onDrop(dragging, 'reorder'); // setTimeout ensures it will run after dragEnd messaging @@ -758,17 +955,18 @@ const ReorderableDrop = memo(function ReorderableDrop( data-test-subj="lnsDragDrop-translatableDrop" className="lnsDragDrop-translatableDrop lnsDragDrop-reorderable" > - +
{ + setActiveDropTarget(undefined); setReorderState((s: ReorderState) => ({ ...s, reorderedItems: [], diff --git a/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 3bd1d5693005c..72771edbae981 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -9,132 +9,340 @@ import { i18n } from '@kbn/i18n'; import { DropType } from '../../types'; import { HumanData } from '.'; -type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; +type AnnouncementFunction = ( + draggedElement: HumanData, + dropElement: HumanData, + announceModifierKeys?: boolean +) => string; interface CustomAnnouncementsType { dropped: Partial<{ [dropType in DropType]: AnnouncementFunction }>; selectedTarget: Partial<{ [dropType in DropType]: AnnouncementFunction }>; } -const selectedTargetReplace = ( - { label }: HumanData, - { label: dropLabel, groupLabel, position }: HumanData -) => - i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replace', { - defaultMessage: `Replace {dropLabel} in {groupLabel} group at position {position} with {label}. Press space or enter to replace`, - values: { - label, - dropLabel, - groupLabel, - position, - }, - }); +const replaceAnnouncement = { + selectedTarget: ( + { label, groupLabel, position }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + canSwap, + canDuplicate, + }: HumanData, + announceModifierKeys?: boolean + ) => { + if (announceModifierKeys && (canSwap || canDuplicate)) { + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceMain', { + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}`, + values: { + label, + groupLabel, + position, + dropLabel, + dropGroupLabel, + dropPosition, + duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', + swapCopy: canSwap ? SWAP_SHORT : '', + }, + }); + } -const droppedReplace = ( - { label }: HumanData, - { label: dropLabel, groupLabel, position }: HumanData -) => - i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { - defaultMessage: 'Replaced {dropLabel} with {label} in {groupLabel} at position {position}', - values: { - label, - dropLabel, - groupLabel, - position, - }, - }); + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replace', { + defaultMessage: `Replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} with {label}. Press space or enter to replace.`, + values: { + label, + dropLabel, + dropGroupLabel, + dropPosition, + }, + }); + }, + dropped: ({ label }: HumanData, { label: dropLabel, groupLabel, position }: HumanData) => + i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { + defaultMessage: 'Replaced {dropLabel} with {label} in {groupLabel} at position {position}', + values: { + label, + dropLabel, + groupLabel, + position, + }, + }), +}; + +const duplicateAnnouncement = { + selectedTarget: ( + { label, groupLabel }: HumanData, + { groupLabel: dropGroupLabel, position }: HumanData + ) => { + if (groupLabel !== dropGroupLabel) { + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicated', { + defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate`, + values: { + label, + dropGroupLabel, + position, + }, + }); + } + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup', { + defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position}. Press space or enter to duplicate`, + values: { + label, + dropGroupLabel, + position, + }, + }); + }, + dropped: ({ label }: HumanData, { groupLabel, position }: HumanData) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicated', { + defaultMessage: 'Duplicated {label} in {groupLabel} group at position {position}', + values: { + label, + groupLabel, + position, + }, + }), +}; + +const reorderAnnouncement = { + selectedTarget: ( + { label, groupLabel, position: prevPosition }: HumanData, + { position }: HumanData + ) => + prevPosition === position + ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', { + defaultMessage: `{label} returned to its initial position {prevPosition}`, + values: { + label, + prevPosition, + }, + }) + : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reordered', { + defaultMessage: `Reorder {label} in {groupLabel} group from position {prevPosition} to position {position}. Press space or enter to reorder`, + values: { + groupLabel, + label, + position, + prevPosition, + }, + }), + dropped: ({ label, groupLabel, position: prevPosition }: HumanData, { position }: HumanData) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', { + defaultMessage: + 'Reordered {label} in {groupLabel} group from position {prevPosition} to position {position}', + values: { + label, + groupLabel, + position, + prevPosition, + }, + }), +}; + +const DUPLICATE_SHORT = i18n.translate('xpack.lens.dragDrop.announce.duplicate.short', { + defaultMessage: ' Hold alt or option to duplicate.', +}); + +const SWAP_SHORT = i18n.translate('xpack.lens.dragDrop.announce.swap.short', { + defaultMessage: ' Hold shift to swap.', +}); export const announcements: CustomAnnouncementsType = { selectedTarget: { - reorder: ({ label, groupLabel, position: prevPosition }, { position }) => - prevPosition === position - ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', { - defaultMessage: `{label} returned to its initial position {prevPosition}`, + reorder: reorderAnnouncement.selectedTarget, + duplicate_compatible: duplicateAnnouncement.selectedTarget, + field_replace: replaceAnnouncement.selectedTarget, + replace_compatible: replaceAnnouncement.selectedTarget, + replace_incompatible: ( + { label, groupLabel, position }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + canSwap, + canDuplicate, + }: HumanData, + announceModifierKeys?: boolean + ) => { + if (announceModifierKeys && (canSwap || canDuplicate)) { + return i18n.translate( + 'xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain', + { + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}`, values: { label, - prevPosition, - }, - }) - : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reordered', { - defaultMessage: `Reorder {label} in {groupLabel} group from position {prevPosition} to position {position}. Press space or enter to reorder`, - values: { groupLabel, - label, position, - prevPosition, + dropLabel, + dropGroupLabel, + dropPosition, + nextLabel, + duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', + swapCopy: canSwap ? SWAP_SHORT : '', }, - }), - duplicate_in_group: ({ label }, { groupLabel, position }) => - i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicated', { - defaultMessage: `Duplicate {label} to {groupLabel} group at position {position}. Press space or enter to duplicate`, + } + ); + } + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible', { + defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Press space or enter to replace`, values: { label, - groupLabel, - position, + nextLabel, + dropLabel, + dropGroupLabel, + dropPosition, }, - }), - field_replace: selectedTargetReplace, - replace_compatible: selectedTargetReplace, - replace_incompatible: ( + }); + }, + move_incompatible: ( + { label, groupLabel, position }: HumanData, + { + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + canSwap, + canDuplicate, + }: HumanData, + announceModifierKeys?: boolean + ) => { + if (announceModifierKeys && (canSwap || canDuplicate)) { + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain', { + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}{swapCopy}`, + values: { + label, + groupLabel, + position, + dropGroupLabel, + dropPosition, + nextLabel, + duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', + swapCopy: canSwap ? SWAP_SHORT : '', + }, + }); + } + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible', { + defaultMessage: `Convert {label} to {nextLabel} and move to {dropGroupLabel} group at position {dropPosition}. Press space or enter to move`, + values: { + label, + nextLabel, + dropGroupLabel, + dropPosition, + }, + }); + }, + + move_compatible: ( + { label, groupLabel, position }: HumanData, + { groupLabel: dropGroupLabel, position: dropPosition, canSwap, canDuplicate }: HumanData, + announceModifierKeys?: boolean + ) => { + if (announceModifierKeys && (canSwap || canDuplicate)) { + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain', { + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group. Press space or enter to move.{duplicateCopy}{swapCopy}`, + values: { + label, + groupLabel, + position, + dropGroupLabel, + dropPosition, + duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', + swapCopy: canSwap ? SWAP_SHORT : '', + }, + }); + } + return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatible', { + defaultMessage: `Move {label} to {dropGroupLabel} group at position {dropPosition}. Press space or enter to move`, + values: { + label, + dropGroupLabel, + dropPosition, + }, + }); + }, + duplicate_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { groupLabel, position, nextLabel }: HumanData ) => - i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible', { - defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position}. Press space or enter to replace`, + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible', { + defaultMessage: + 'Convert copy of {label} to {nextLabel} and add to {groupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate', values: { label, - nextLabel, - dropLabel, groupLabel, position, + nextLabel, }, }), - move_incompatible: ( + replace_duplicate_incompatible: ( { label }: HumanData, - { label: groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel }: HumanData ) => - i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible', { - defaultMessage: `Convert {label} to {nextLabel} and move to {groupLabel} group at position {position}. Press space or enter to move`, + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible', { + defaultMessage: + 'Convert copy of {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate and replace', values: { label, - nextLabel, groupLabel, position, + dropLabel, + nextLabel, }, }), - move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) => - i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatible', { - defaultMessage: `Move {label} to {groupLabel} group at position {position}. Press space or enter to move`, + replace_duplicate_compatible: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible', { + defaultMessage: + 'Duplicate {label} and replace {dropLabel} in {groupLabel} at position {position}. Hold Alt or Option and press space or enter to duplicate and replace', values: { label, + dropLabel, groupLabel, position, }, }), - }, - dropped: { - reorder: ({ label, groupLabel, position: prevPosition }, { position }) => - i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', { + swap_compatible: ( + { label, groupLabel, position }: HumanData, + { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.swapCompatible', { defaultMessage: - 'Reordered {label} in {groupLabel} group from position {prevPosition} to positon {position}', + 'Swap {label} in {groupLabel} group at position {position} with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Shift and press space or enter to swap', values: { label, groupLabel, position, - prevPosition, + dropLabel, + dropGroupLabel, + dropPosition, }, }), - duplicate_in_group: ({ label }, { groupLabel, position }) => - i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicated', { - defaultMessage: 'Duplicated {label} in {groupLabel} group at position {position}', + swap_incompatible: ( + { label, groupLabel, position }: HumanData, + { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible', { + defaultMessage: + 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} and swap with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Shift and press space or enter to swap', values: { label, groupLabel, position, + dropLabel, + dropGroupLabel, + dropPosition, + nextLabel, }, }), - field_replace: droppedReplace, - replace_compatible: droppedReplace, + }, + dropped: { + reorder: reorderAnnouncement.dropped, + duplicate_compatible: duplicateAnnouncement.dropped, + field_replace: replaceAnnouncement.dropped, + replace_compatible: replaceAnnouncement.dropped, replace_incompatible: ( { label }: HumanData, { label: dropLabel, groupLabel, position, nextLabel }: HumanData @@ -171,6 +379,84 @@ export const announcements: CustomAnnouncementsType = { position, }, }), + + duplicate_incompatible: ( + { label }: HumanData, + { groupLabel, position, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicateIncompatible', { + defaultMessage: + 'Converted copy of {label} to {nextLabel} and added to {groupLabel} group at position {position}', + values: { + label, + groupLabel, + position, + nextLabel, + }, + }), + + replace_duplicate_incompatible: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible', { + defaultMessage: + 'Converted copy of {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}', + values: { + label, + dropLabel, + groupLabel, + position, + nextLabel, + }, + }), + replace_duplicate_compatible: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible', { + defaultMessage: + 'Replaced {dropLabel} with a copy of {label} in {groupLabel} at position {position}', + values: { + label, + dropLabel, + groupLabel, + position, + }, + }), + swap_compatible: ( + { label, groupLabel, position }: HumanData, + { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.swapCompatible', { + defaultMessage: + 'Moved {label} to {dropGroupLabel} at position {dropPosition} and {dropLabel} to {groupLabel} group at position {position}', + values: { + label, + groupLabel, + position, + dropLabel, + dropGroupLabel, + dropPosition, + }, + }), + swap_incompatible: ( + { label, groupLabel, position }: HumanData, + { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.swapIncompatible', { + defaultMessage: + 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and swapped with {dropLabel} in {dropGroupLabel} group at position {dropPosition}', + values: { + label, + groupLabel, + position, + dropGroupLabel, + dropLabel, + dropPosition, + nextLabel, + }, + }), }, }; @@ -256,7 +542,13 @@ export const announce = { dropped: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) => (type && announcements.dropped?.[type]?.(draggedElement, dropElement)) || defaultAnnouncements.dropped(draggedElement, dropElement), - selectedTarget: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) => - (type && announcements.selectedTarget?.[type]?.(draggedElement, dropElement)) || + selectedTarget: ( + draggedElement: HumanData, + dropElement: HumanData, + type?: DropType, + announceModifierKeys?: boolean + ) => + (type && + announcements.selectedTarget?.[type]?.(draggedElement, dropElement, announceModifierKeys)) || defaultAnnouncements.selectedTarget(draggedElement, dropElement), }; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx index 2c6b07ea11765..4db19e10ec701 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx @@ -135,11 +135,31 @@ export function nextValidDropTarget( return; } - const filteredTargets = Object.entries(dropTargetsByOrder).filter( - ([, dropTarget]) => dropTarget && filterElements(dropTarget) + const filteredTargets: Array<[string, DropIdentifier | undefined]> = Object.entries( + dropTargetsByOrder + ).filter(([, dropTarget]) => { + return dropTarget && filterElements(dropTarget); + }); + + // filter out secondary targets + const uniqueIdTargets = filteredTargets.reduce( + ( + acc: Array<[string, DropIdentifier | undefined]>, + current: [string, DropIdentifier | undefined] + ) => { + const [, currentDropTarget] = current; + if (!currentDropTarget) { + return acc; + } + if (acc.find(([, target]) => target?.id === currentDropTarget.id)) { + return acc; + } + return [...acc, current]; + }, + [] ); - const nextDropTargets = [...filteredTargets, draggingOrder].sort(([orderA], [orderB]) => { + const nextDropTargets = [...uniqueIdTargets, draggingOrder].sort(([orderA], [orderB]) => { const parsedOrderA = orderA.split(',').map((v) => Number(v)); const parsedOrderB = orderB.split(',').map((v) => Number(v)); diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx index 11f460a400dcd..8b28affa45596 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -12,6 +12,13 @@ export interface HumanData { groupLabel?: string; position?: number; nextLabel?: string; + canSwap?: boolean; + canDuplicate?: boolean; +} + +export interface Ghost { + children: React.ReactElement; + style: React.CSSProperties; } export type DragDropIdentifier = Record & { @@ -23,10 +30,7 @@ export type DragDropIdentifier = Record & { }; export type DraggingIdentifier = DragDropIdentifier & { - ghost?: { - children: React.ReactElement; - style: React.CSSProperties; - }; + ghost?: Ghost; }; export type DropIdentifier = DragDropIdentifier & { diff --git a/x-pack/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md index 01cc4c7bc85a5..d7183263c519b 100644 --- a/x-pack/plugins/lens/public/drag_drop/readme.md +++ b/x-pack/plugins/lens/public/drag_drop/readme.md @@ -48,7 +48,7 @@ To enable dragging an item, use `DragDrop` with both a `draggable` and a `value` ## Dropping -To enable dropping, use `DragDrop` with both a `droppable` attribute and an `onDrop` handler attribute. Droppable should only be set to true if there is an item being dragged, and if a drop of the dragged item is supported. +To enable dropping, use `DragDrop` with both a `dropTypes` attribute that should be an array with at least one value and an `onDrop` handler attribute. `dropType` should only be truthy if is an item being dragged, and if a drop of the dragged item is supported. ```js const { dragging } = useContext(DragContext); @@ -56,7 +56,7 @@ const { dragging } = useContext(DragContext); return ( onChange([...items, item])} > {items.map((x) => ( @@ -85,8 +85,7 @@ The children `DragDrop` components must have props defined as in the example: i18n.translate('xpack.lens.configure.editConfig', { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx similarity index 72% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index 8449727a9e79d..212b1794d94ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -5,32 +5,20 @@ * 2.0. */ -import React, { useMemo, useCallback, useContext } from 'react'; -import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; - +import React, { useMemo, useCallback, useContext, ReactElement } from 'react'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop'; import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation, DropType, -} from '../../../types'; -import { LayerDatasourceDropProps } from './types'; - -const getAdditionalClassesOnEnter = (dropType?: string) => { - if ( - dropType === 'field_replace' || - dropType === 'replace_compatible' || - dropType === 'replace_incompatible' - ) { - return 'lnsDragDrop-isReplacing'; - } -}; - -const getAdditionalClassesOnDroppable = (dropType?: string) => { - if (dropType === 'move_incompatible' || dropType === 'replace_incompatible') { - return 'lnsDragDrop-notCompatible'; - } -}; +} from '../../../../types'; +import { LayerDatasourceDropProps } from '../types'; +import { + getCustomDropTarget, + getAdditionalClassesOnDroppable, + getAdditionalClassesOnEnter, +} from './drop_targets_utils'; export function DraggableDimensionButton({ layerId, @@ -58,7 +46,7 @@ export function DraggableDimensionButton({ group: VisualizationDimensionGroupConfig; groups: VisualizationDimensionGroupConfig[]; label: string; - children: React.ReactElement; + children: ReactElement; layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; accessorIndex: number; @@ -76,8 +64,18 @@ export function DraggableDimensionButton({ dimensionGroups: groups, }); - const dropType = dropProps?.dropType; + const dropTypes = dropProps?.dropTypes; const nextLabel = dropProps?.nextLabel; + const canDuplicate = !!( + dropTypes && + (dropTypes.includes('replace_duplicate_incompatible') || + dropTypes.includes('replace_duplicate_compatible')) + ); + + const canSwap = !!( + dropTypes && + (dropTypes.includes('swap_incompatible') || dropTypes.includes('swap_compatible')) + ); const value = useMemo( () => ({ @@ -85,15 +83,28 @@ export function DraggableDimensionButton({ groupId: group.groupId, layerId, id: columnId, - dropType, + filterOperations: group.filterOperations, humanData: { + canSwap, + canDuplicate, label, groupLabel: group.groupLabel, position: accessorIndex + 1, nextLabel: nextLabel || '', }, }), - [columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel, nextLabel] + [ + columnId, + group.groupId, + accessorIndex, + layerId, + label, + group.groupLabel, + nextLabel, + group.filterOperations, + canDuplicate, + canSwap, + ] ); // todo: simplify by id and use drop targets? @@ -110,7 +121,7 @@ export function DraggableDimensionButton({ columnId, ]); - const handleOnDrop = React.useCallback( + const handleOnDrop = useCallback( (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), [value, onDrop] ); @@ -122,12 +133,13 @@ export function DraggableDimensionButton({ data-test-subj={group.dataTestSubj} > 1 ? reorderableGroup : undefined} value={value} onDrop={handleOnDrop} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx new file mode 100644 index 0000000000000..85934412dd374 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 classNames from 'classnames'; +import { EuiIcon, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DropType } from '../../../../types'; + +const getExtraDrop = ({ + type, + isIncompatible, +}: { + type: 'swap' | 'duplicate'; + isIncompatible?: boolean; +}) => { + return ( + + + + + + + + + {type === 'duplicate' + ? i18n.translate('xpack.lens.dragDrop.duplicate', { + defaultMessage: 'Duplicate', + }) + : i18n.translate('xpack.lens.dragDrop.swap', { + defaultMessage: 'Swap', + })} + + + + + + + + {' '} + {type === 'duplicate' + ? i18n.translate('xpack.lens.dragDrop.altOption', { + defaultMessage: 'Alt/Option', + }) + : i18n.translate('xpack.lens.dragDrop.shift', { + defaultMessage: 'Shift', + })} + + + + + ); +}; + +const customDropTargetsMap: Partial<{ [dropType in DropType]: React.ReactElement }> = { + replace_duplicate_incompatible: getExtraDrop({ type: 'duplicate', isIncompatible: true }), + duplicate_incompatible: getExtraDrop({ type: 'duplicate', isIncompatible: true }), + swap_incompatible: getExtraDrop({ type: 'swap', isIncompatible: true }), + replace_duplicate_compatible: getExtraDrop({ type: 'duplicate' }), + duplicate_compatible: getExtraDrop({ type: 'duplicate' }), + swap_compatible: getExtraDrop({ type: 'swap' }), +}; + +export const getCustomDropTarget = (dropType: DropType) => customDropTargetsMap?.[dropType] || null; + +export const getAdditionalClassesOnEnter = (dropType?: string) => { + if ( + dropType && + [ + 'field_replace', + 'replace_compatible', + 'replace_incompatible', + 'replace_duplicate_compatible', + 'replace_duplicate_incompatible', + ].includes(dropType) + ) { + return 'lnsDragDrop-isReplacing'; + } +}; + +export const getAdditionalClassesOnDroppable = (dropType?: string) => { + if ( + dropType && + [ + 'move_incompatible', + 'replace_incompatible', + 'swap_incompatible', + 'duplicate_incompatible', + 'replace_duplicate_incompatible', + ].includes(dropType) + ) { + return 'lnsDragDrop-notCompatible'; + } +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx similarity index 84% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index a6ccac1427fbf..cb72b986430d6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -9,22 +9,17 @@ import React, { useMemo, useState, useEffect, useContext } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { generateId } from '../../../id_generator'; -import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; +import { generateId } from '../../../../id_generator'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop'; -import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types'; -import { LayerDatasourceDropProps } from './types'; +import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../../types'; +import { LayerDatasourceDropProps } from '../types'; +import { getCustomDropTarget, getAdditionalClassesOnDroppable } from './drop_targets_utils'; const label = i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', { defaultMessage: 'Empty dimension', }); -const getAdditionalClassesOnDroppable = (dropType?: string) => { - if (dropType === 'move_incompatible' || dropType === 'replace_incompatible') { - return 'lnsDragDrop-notCompatible'; - } -}; - export function EmptyDimensionButton({ group, groups, @@ -69,24 +64,29 @@ export function EmptyDimensionButton({ dimensionGroups: groups, }); - const dropType = dropProps?.dropType; + const dropTypes = dropProps?.dropTypes; const nextLabel = dropProps?.nextLabel; + const canDuplicate = !!( + dropTypes && + (dropTypes.includes('duplicate_compatible') || dropTypes.includes('duplicate_incompatible')) + ); + const value = useMemo( () => ({ columnId: newColumnId, groupId: group.groupId, layerId, id: newColumnId, - dropType, humanData: { label, groupLabel: group.groupLabel, position: itemIndex + 1, nextLabel: nextLabel || '', + canDuplicate, }, }), - [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel] + [newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel, canDuplicate] ); const handleOnDrop = React.useCallback( @@ -101,7 +101,8 @@ export function EmptyDimensionButton({ value={value} order={[2, layerIndex, groupIndex, itemIndex]} onDrop={handleOnDrop} - dropType={dropType} + dropTypes={dropTypes} + getCustomDropTarget={getCustomDropTarget} >
{ + container = document.createElement('div'); + container.id = 'lensContainer'; + document.body.appendChild(container); +}); + +afterEach(() => { + if (container && container.parentNode) { + container.parentNode.removeChild(container); + } + + container = undefined; +}); + describe('ConfigPanel', () => { let mockVisualization: jest.Mocked; let mockVisualization2: jest.Mocked; @@ -105,7 +121,9 @@ describe('ConfigPanel', () => { describe('focus behavior when adding or removing layers', () => { it('should focus the only layer when resetting the layer', () => { - const component = mountWithIntl(); + const component = mountWithIntl(, { + attachTo: container, + }); const firstLayerFocusable = component .find(LayerPanel) .first() @@ -126,7 +144,7 @@ describe('ConfigPanel', () => { first: mockDatasource.publicAPIMock, second: mockDatasource.publicAPIMock, }; - const component = mountWithIntl(); + const component = mountWithIntl(, { attachTo: container }); const secondLayerFocusable = component .find(LayerPanel) .at(1) @@ -147,7 +165,7 @@ describe('ConfigPanel', () => { first: mockDatasource.publicAPIMock, second: mockDatasource.publicAPIMock, }; - const component = mountWithIntl(); + const component = mountWithIntl(, { attachTo: container }); const firstLayerFocusable = component .find(LayerPanel) .first() @@ -169,7 +187,9 @@ describe('ConfigPanel', () => { } }); - const component = mountWithIntl(); + const component = mountWithIntl(, { + attachTo: container, + }); act(() => { component.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click'); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index ec4c2adba8fd7..788bf049b779b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -36,6 +36,7 @@ .lnsLayerPanel__group { padding: $euiSizeS 0; + margin-bottom: $euiSizeS; } .lnsLayerPanel__group:empty { @@ -66,8 +67,6 @@ } .lnsLayerPanel__dimension--empty { - margin-top: $euiSizeS; - &:focus, &:focus-within { @include euiFocusRing; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 30740bbd6b217..7ee7a27a53c7d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -7,22 +7,38 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import { EuiFormRow } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { Visualization } from '../../../types'; +import { LayerPanel } from './layer_panel'; +import { ChildDragDropProvider, DragDrop } from '../../../drag_drop'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { generateId } from '../../../id_generator'; import { createMockVisualization, createMockFramePublicAPI, createMockDatasource, DatasourceMock, } from '../../mocks'; -import { ChildDragDropProvider, DragDrop } from '../../../drag_drop'; -import { EuiFormRow } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test/jest'; -import { Visualization } from '../../../types'; -import { LayerPanel } from './layer_panel'; -import { coreMock } from 'src/core/public/mocks'; -import { generateId } from '../../../id_generator'; jest.mock('../../../id_generator'); +let container: HTMLDivElement | undefined; + +beforeEach(() => { + container = document.createElement('div'); + container.id = 'lensContainer'; + document.body.appendChild(container); +}); + +afterEach(() => { + if (container && container.parentNode) { + container.parentNode.removeChild(container); + } + + container = undefined; +}); + const defaultContext = { dragging: undefined, setDragging: jest.fn(), @@ -453,7 +469,7 @@ describe('LayerPanel', () => { }); mockDatasource.getDropProps.mockReturnValue({ - dropType: 'field_add', + dropTypes: ['field_add'], nextLabel: '', }); @@ -480,7 +496,12 @@ describe('LayerPanel', () => { }) ); - component.find('[data-test-subj="lnsGroup"] DragDrop .lnsDragDrop').first().simulate('drop'); + const dragDropElement = component + .find('[data-test-subj="lnsGroup"] DragDrop .lnsDragDrop') + .first(); + + dragDropElement.simulate('dragOver'); + dragDropElement.simulate('drop'); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ @@ -504,7 +525,7 @@ describe('LayerPanel', () => { }); mockDatasource.getDropProps.mockImplementation(({ columnId }) => - columnId !== 'a' ? { dropType: 'field_replace', nextLabel: '' } : undefined + columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined ); const draggingField = { @@ -532,11 +553,13 @@ describe('LayerPanel', () => { component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType') ).toEqual(undefined); - component + const dragDropElement = component .find('[data-test-subj="lnsGroup"] DragDrop') .first() - .find('.lnsLayerPanel__dimension') - .simulate('drop'); + .find('.lnsLayerPanel__dimension'); + + dragDropElement.simulate('dragOver'); + dragDropElement.simulate('drop'); expect(mockDatasource.onDrop).not.toHaveBeenCalled(); }); @@ -566,7 +589,7 @@ describe('LayerPanel', () => { }); mockDatasource.getDropProps.mockReturnValue({ - dropType: 'replace_compatible', + dropTypes: ['replace_compatible'], nextLabel: '', }); @@ -595,7 +618,13 @@ describe('LayerPanel', () => { ); // Simulate drop on the pre-populated dimension - component.find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop').at(0).simulate('drop'); + + const dragDropElement = component + .find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop') + .at(0); + dragDropElement.simulate('dragOver'); + dragDropElement.simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'b', @@ -604,7 +633,14 @@ describe('LayerPanel', () => { ); // Simulate drop on the empty dimension - component.find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop').at(1).simulate('drop'); + + const updatedDragDropElement = component + .find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop') + .at(2); + + updatedDragDropElement.simulate('dragOver'); + updatedDragDropElement.simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', @@ -642,7 +678,8 @@ describe('LayerPanel', () => { const component = mountWithIntl( - + , + { attachTo: container } ); act(() => { component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); @@ -695,12 +732,12 @@ describe('LayerPanel', () => { ); act(() => { - component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_in_group'); + component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', - dropType: 'duplicate_in_group', + dropType: 'duplicate_compatible', droppedItem: draggingOperation, }) ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 21115285b5ce0..cf3c9099d4b0d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -25,9 +25,9 @@ import { trackUiEvent } from '../../../lens_ui_telemetry'; import { LayerPanelProps, ActiveDimensionState } from './types'; import { DimensionContainer } from './dimension_container'; import { RemoveLayerButton } from './remove_layer_button'; -import { EmptyDimensionButton } from './empty_dimension_button'; -import { DimensionButton } from './dimension_button'; -import { DraggableDimensionButton } from './draggable_dimension_button'; +import { EmptyDimensionButton } from './buttons/empty_dimension_button'; +import { DimensionButton } from './buttons/dimension_button'; +import { DraggableDimensionButton } from './buttons/draggable_dimension_button'; import { useFocusUpdate } from './use_focus_update'; const initialActiveDimensionState = { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss index a8d8146afebb2..ffc0adf3e33ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss @@ -28,7 +28,8 @@ // Leave out left padding so the left sidebar's focus states are visible outside of content bounds // This also means needing to add same amount of margin to page content and suggestion items padding: $euiSize $euiSize 0; - + position: relative; + z-index: $euiZLevel1; &:first-child { padding-left: $euiSize; } @@ -55,5 +56,7 @@ padding: $euiSize $euiSizeXS $euiSize $euiSize; overflow-x: hidden; overflow-y: scroll; + padding-left: $euiFormMaxWidth + $euiSize; + margin-left: -$euiFormMaxWidth; } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 5e5cfc3402f10..e741b9ee243db 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -955,7 +955,7 @@ describe('workspace_panel', () => { visualizationState: {}, }); initComponent(); - expect(instance.find(DragDrop).prop('dropType')).toBeTruthy(); + expect(instance.find(DragDrop).prop('dropTypes')).toBeTruthy(); }); it('should refuse to drop if there are no suggestions', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index b15b659f2d221..8a0b9922c736b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -354,7 +354,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ className="lnsWorkspacePanel__dragDrop" dataTestSubj="lnsWorkspace" draggable={false} - dropType={suggestionForDraggedField ? 'field_add' : undefined} + dropTypes={suggestionForDraggedField ? ['field_add'] : undefined} onDrop={onDrop} value={dropProps.value} order={dropProps.order} diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 925358c434e5d..157975b630e1e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -338,7 +338,13 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(2); }); - it('should re-render when dashboard view/edit mode changes', async () => { + it('should re-render when dashboard view/edit mode changes if dynamic actions are set', async () => { + const sampleInput = ({ + id: '123', + enhancements: { + dynamicActions: {}, + }, + } as unknown) as LensEmbeddableInput; const embeddable = new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -371,6 +377,13 @@ describe('embeddable', () => { viewMode: ViewMode.VIEW, }); + expect(expressionRenderer).toHaveBeenCalledTimes(1); + + embeddable.updateInput({ + ...sampleInput, + viewMode: ViewMode.VIEW, + }); + expect(expressionRenderer).toHaveBeenCalledTimes(2); }); @@ -679,4 +692,205 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(1); }); + + it('should call onload after rerender and onData$ call ', async () => { + const onLoad = jest.fn(); + + expressionRenderer = jest.fn(({ onData$ }) => { + setTimeout(() => { + onData$?.({}); + }, 10); + + return null; + }); + + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + }, + ({ id: '123', onLoad } as unknown) as LensEmbeddableInput + ); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + expect(onLoad).toHaveBeenCalledWith(true); + expect(onLoad).toHaveBeenCalledTimes(1); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + // loading should become false + expect(onLoad).toHaveBeenCalledTimes(2); + expect(onLoad).toHaveBeenNthCalledWith(2, false); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + + embeddable.updateInput({ + searchSessionId: 'newSession', + }); + embeddable.reload(); + + // loading should become again true + expect(onLoad).toHaveBeenCalledTimes(3); + expect(onLoad).toHaveBeenNthCalledWith(3, true); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + // loading should again become false + expect(onLoad).toHaveBeenCalledTimes(4); + expect(onLoad).toHaveBeenNthCalledWith(4, false); + }); + + it('should call onFilter event on filter call ', async () => { + const onFilter = jest.fn(); + + expressionRenderer = jest.fn(({ onEvent }) => { + setTimeout(() => { + onEvent?.({ name: 'filter', data: { pings: false } }); + }, 10); + + return null; + }); + + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + }, + ({ id: '123', onFilter } as unknown) as LensEmbeddableInput + ); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(onFilter).toHaveBeenCalledWith({ pings: false }); + expect(onFilter).toHaveBeenCalledTimes(1); + }); + + it('should call onBrush event on brushing', async () => { + const onBrushEnd = jest.fn(); + + expressionRenderer = jest.fn(({ onEvent }) => { + setTimeout(() => { + onEvent?.({ name: 'brush', data: { range: [0, 1] } }); + }, 10); + + return null; + }); + + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + }, + ({ id: '123', onBrushEnd } as unknown) as LensEmbeddableInput + ); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(onBrushEnd).toHaveBeenCalledWith({ range: [0, 1] }); + expect(onBrushEnd).toHaveBeenCalledTimes(1); + }); + + it('should call onTableRowClick event ', async () => { + const onTableRowClick = jest.fn(); + + expressionRenderer = jest.fn(({ onEvent }) => { + setTimeout(() => { + onEvent?.({ name: 'tableRowContextMenuClick', data: { name: 'test' } }); + }, 10); + + return null; + }); + + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + }, + ({ id: '123', onTableRowClick } as unknown) as LensEmbeddableInput + ); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(onTableRowClick).toHaveBeenCalledWith({ name: 'test' }); + expect(onTableRowClick).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index bfeb645d7bd5f..1db067606dc82 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -45,6 +45,9 @@ import { isLensBrushEvent, isLensFilterEvent, isLensTableRowContextMenuClickEvent, + LensBrushEvent, + LensFilterEvent, + LensTableRowContextMenuEvent, } from '../../types'; import { IndexPatternsContract } from '../../../../../../src/plugins/data/public'; @@ -63,6 +66,10 @@ interface LensBaseEmbeddableInput extends EmbeddableInput { renderMode?: RenderMode; style?: React.CSSProperties; className?: string; + onBrushEnd?: (data: LensBrushEvent['data']) => void; + onLoad?: (isLoading: boolean) => void; + onFilter?: (data: LensFilterEvent['data']) => void; + onTableRowClick?: (data: LensTableRowContextMenuEvent['data']) => void; } export type LensByValueInput = { @@ -103,6 +110,8 @@ export class Embeddable private isInitialized = false; private activeData: Partial | undefined; private errors: ErrorMessage[] | undefined; + private inputReloadSubscriptions: Subscription[]; + private isDestroyed?: boolean; private externalSearchContext: { timeRange?: TimeRange; @@ -133,65 +142,79 @@ export class Embeddable const input$ = this.getInput$(); + this.inputReloadSubscriptions = []; + // Lens embeddable does not re-render when embeddable input changes in // general, to improve performance. This line makes sure the Lens embeddable // re-renders when anything in ".dynamicActions" (e.g. drilldowns) changes. - input$ - .pipe( - map((input) => input.enhancements?.dynamicActions), - distinctUntilChanged((a, b) => isEqual(a, b)), - skip(1) - ) - .subscribe((input) => { - this.reload(); - }); + this.inputReloadSubscriptions.push( + input$ + .pipe( + map((input) => input.enhancements?.dynamicActions), + distinctUntilChanged((a, b) => isEqual(a, b)), + skip(1) + ) + .subscribe((input) => { + this.reload(); + }) + ); // Lens embeddable does not re-render when embeddable input changes in // general, to improve performance. This line makes sure the Lens embeddable // re-renders when dashboard view mode switches between "view/edit". This is // needed to see the changes to ".dynamicActions" (e.g. drilldowns) when // dashboard's mode is toggled. - input$ - .pipe( - map((input) => input.viewMode), - distinctUntilChanged(), - skip(1) - ) - .subscribe((input) => { - this.reload(); - }); + this.inputReloadSubscriptions.push( + input$ + .pipe( + map((input) => input.viewMode), + distinctUntilChanged(), + skip(1) + ) + .subscribe((input) => { + // only reload if drilldowns are set + if (this.getInput().enhancements?.dynamicActions) { + this.reload(); + } + }) + ); // Re-initialize the visualization if either the attributes or the saved object id changes - input$ - .pipe( - distinctUntilChanged((a, b) => - isEqual( - ['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId], - ['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId] - ) - ), - skip(1) - ) - .subscribe(async (input) => { - await this.initializeSavedVis(input); - this.reload(); - }); + + this.inputReloadSubscriptions.push( + input$ + .pipe( + distinctUntilChanged((a, b) => + isEqual( + ['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId], + ['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId] + ) + ), + skip(1) + ) + .subscribe(async (input) => { + await this.initializeSavedVis(input); + this.reload(); + }) + ); // Update search context and reload on changes related to search - this.getUpdated$() - .pipe(map(() => this.getInput())) - .pipe( - distinctUntilChanged((a, b) => - isEqual( - [a.filters, a.query, a.timeRange, a.searchSessionId], - [b.filters, b.query, b.timeRange, b.searchSessionId] - ) - ), - skip(1) - ) - .subscribe(async (input) => { - this.onContainerStateChanged(input); - }); + this.inputReloadSubscriptions.push( + this.getUpdated$() + .pipe(map(() => this.getInput())) + .pipe( + distinctUntilChanged((a, b) => + isEqual( + [a.filters, a.query, a.timeRange, a.searchSessionId], + [b.filters, b.query, b.timeRange, b.searchSessionId] + ) + ), + skip(1) + ) + .subscribe(async (input) => { + this.onContainerStateChanged(input); + }) + ); } public supportedTriggers() { @@ -222,7 +245,7 @@ export class Embeddable this.onFatalError(e); return false; }); - if (!attributes) { + if (!attributes || this.isDestroyed) { return; } this.savedVis = { @@ -268,6 +291,10 @@ export class Embeddable inspectorAdapters?: Partial | undefined ) => { this.activeData = inspectorAdapters; + if (this.input.onLoad) { + // once onData$ is get's called from expression renderer, loading becomes false + this.input.onLoad(false); + } }; /** @@ -277,9 +304,12 @@ export class Embeddable */ render(domNode: HTMLElement | Element) { this.domNode = domNode; - if (!this.savedVis || !this.isInitialized) { + if (!this.savedVis || !this.isInitialized || this.isDestroyed) { return; } + if (this.input.onLoad) { + this.input.onLoad(true); + } const input = this.getInput(); render( 0) { + this.inputReloadSubscriptions.forEach((reloadSub) => { + reloadSub.unsubscribe(); + }); + } if (this.domNode) { unmountComponentAtNode(this.domNode); } diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index fa5a9f9289e92..9b53e59f96792 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -19,7 +19,9 @@ export type { SeriesType, ValueLabelConfig, YAxisMode, + XYCurveType, } from './xy_visualization/types'; +export type { DataType } from './types'; export type { PieVisualizationState, PieLayerState, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index fef8ee171830d..e6a38ce2bb713 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -173,7 +173,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'memory', }, }, @@ -200,7 +200,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -827,7 +827,7 @@ describe('IndexPattern Data Panel', () => { }); // wait for indx pattern to be loaded - await new Promise((r) => setTimeout(r, 0)); + await act(async () => await new Promise((r) => setTimeout(r, 0))); expect(props.indexPatternFieldEditor.openEditor).toHaveBeenCalledWith( expect.objectContaining({ @@ -860,10 +860,11 @@ describe('IndexPattern Data Panel', () => { .prop('children') as ReactElement).props.items[0].props.onClick(); }); // wait for indx pattern to be loaded - await new Promise((r) => setTimeout(r, 0)); + await act(async () => await new Promise((r) => setTimeout(r, 0))); + await (props.indexPatternFieldEditor.openEditor as jest.Mock).mock.calls[0][0].onSave(); // wait for indx pattern to be loaded - await new Promise((r) => setTimeout(r, 0)); + await act(async () => await new Promise((r) => setTimeout(r, 0))); expect(props.onUpdateIndexPattern).toHaveBeenCalledWith( expect.objectContaining({ fields: [ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx index ccdb86d250962..c6ecdd73cb6ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx @@ -134,7 +134,7 @@ describe('BucketNestingEditor', () => { layer={{ columnOrder: ['a', 'b', 'c'], columns: { - a: mockCol({ operationType: 'avg', isBucketed: false }), + a: mockCol({ operationType: 'average', isBucketed: false }), b: mockCol({ operationType: 'max', isBucketed: false }), c: mockCol({ operationType: 'min', isBucketed: false }), }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index d586818cb3c11..7d1644d07d2aa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -372,7 +372,7 @@ describe('IndexPatternDimensionEditorPanel', () => { label: 'Unique count of source', dataType: 'number', isBucketed: false, - operationType: 'cardinality', + operationType: 'unique_count', sourceField: 'source,', }, })} @@ -414,7 +414,7 @@ describe('IndexPatternDimensionEditorPanel', () => { const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; - expect(items.find(({ id }) => id === 'derivative')!['data-test-subj']).toContain( + expect(items.find(({ id }) => id === 'differences')!['data-test-subj']).toContain( 'incompatible' ); expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( @@ -462,7 +462,7 @@ describe('IndexPatternDimensionEditorPanel', () => { 'incompatible' ); - expect(items.find(({ id }) => id === 'derivative')!['data-test-subj']).not.toContain( + expect(items.find(({ id }) => id === 'differences')!['data-test-subj']).not.toContain( 'incompatible' ); expect(items.find(({ id }) => id === 'moving_average')!['data-test-subj']).not.toContain( @@ -817,7 +817,7 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should select compatible operation if field not compatible with selected operation', () => { wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); expect(setState).toHaveBeenCalledWith( { ...state, @@ -825,7 +825,7 @@ describe('IndexPatternDimensionEditorPanel', () => { first: { ...state.layers.first, incompleteColumns: { - col2: { operationType: 'avg' }, + col2: { operationType: 'average' }, }, }, }, @@ -838,7 +838,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .filter('[data-test-subj="indexPattern-dimension-field"]'); const options = comboBox.prop('options'); - // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation + // options[1][2] is a `source` field of type `string` which doesn't support `average` operation act(() => { comboBox.prop('onChange')!([options![1].options![2]]); }); @@ -885,7 +885,7 @@ describe('IndexPatternDimensionEditorPanel', () => { // Transition to a field operation (incompatible) wrapper - .find('button[data-test-subj="lns-indexPatternDimension-avg incompatible"]') + .find('button[data-test-subj="lns-indexPatternDimension-average incompatible"]') .simulate('click'); // Now check that the dimension gets cleaned up on state update @@ -896,7 +896,7 @@ describe('IndexPatternDimensionEditorPanel', () => { first: { ...state.layers.first, incompleteColumns: { - col2: { operationType: 'avg' }, + col2: { operationType: 'average' }, }, }, }, @@ -914,7 +914,7 @@ describe('IndexPatternDimensionEditorPanel', () => { dataType: 'number', isBucketed: false, label: '', - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, })} @@ -1037,7 +1037,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = shallow( @@ -1143,7 +1143,7 @@ describe('IndexPatternDimensionEditorPanel', () => { label: 'Sum of bytes per hour', }); wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); expect(props.setState).toHaveBeenCalledWith( { ...props.state, @@ -1478,7 +1478,7 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should support selecting the operation before the field', () => { wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); expect(setState).toHaveBeenCalledWith( { @@ -1488,7 +1488,7 @@ describe('IndexPatternDimensionEditorPanel', () => { ...state.layers.first, incompleteColumns: { col2: { - operationType: 'avg', + operationType: 'average', }, }, }, @@ -1516,7 +1516,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }), }, @@ -1547,7 +1547,7 @@ describe('IndexPatternDimensionEditorPanel', () => { /> ); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); expect(setState).toHaveBeenCalledWith( { @@ -1559,7 +1559,7 @@ describe('IndexPatternDimensionEditorPanel', () => { ...initialState.layers.first.columns, col2: expect.objectContaining({ sourceField: 'bytes', - operationType: 'avg', + operationType: 'average', // Other parts of this don't matter for this test }), }, @@ -1601,7 +1601,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount(); act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); }); const options = wrapper @@ -1790,7 +1790,7 @@ describe('IndexPatternDimensionEditorPanel', () => { dataType: 'number', isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'memory', }, }); @@ -1831,7 +1831,7 @@ describe('IndexPatternDimensionEditorPanel', () => { dataType: 'number', isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'memory', params: { format: { id: 'bytes', params: { decimals: 0 } }, @@ -1871,7 +1871,7 @@ describe('IndexPatternDimensionEditorPanel', () => { dataType: 'number', isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'memory', params: { format: { id: 'bytes', params: { decimals: 2 } }, @@ -1914,7 +1914,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(wrapper.find('ReferenceEditor')).toHaveLength(0); wrapper - .find('button[data-test-subj="lns-indexPatternDimension-derivative incompatible"]') + .find('button[data-test-subj="lns-indexPatternDimension-differences incompatible"]') .simulate('click'); expect(wrapper.find('ReferenceEditor')).toHaveLength(1); @@ -1926,7 +1926,7 @@ describe('IndexPatternDimensionEditorPanel', () => { label: 'Differences of (incomplete)', dataType: 'number', isBucketed: false, - operationType: 'derivative', + operationType: 'differences', references: ['col2'], params: {}, }, @@ -1939,7 +1939,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(wrapper.find('ReferenceEditor')).toHaveLength(1); wrapper - .find('button[data-test-subj="lns-indexPatternDimension-avg incompatible"]') + .find('button[data-test-subj="lns-indexPatternDimension-average incompatible"]') .simulate('click'); expect(wrapper.find('ReferenceEditor')).toHaveLength(0); @@ -1948,10 +1948,10 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should show a warning when the current dimension is no longer configurable', () => { const stateWithInvalidCol: IndexPatternPrivateState = getStateWithColumns({ col1: { - label: 'Invalid derivative', + label: 'Invalid differences', dataType: 'number', isBucketed: false, - operationType: 'derivative', + operationType: 'differences', references: ['ref1'], }, }); @@ -1962,7 +1962,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect( wrapper - .find('[data-test-subj="lns-indexPatternDimension-derivative incompatible"]') + .find('[data-test-subj="lns-indexPatternDimension-differences incompatible"]') .find('EuiText[color="danger"]') .first() ).toBeTruthy(); @@ -1975,7 +1975,7 @@ describe('IndexPatternDimensionEditorPanel', () => { label: 'Avg', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }), @@ -2010,6 +2010,8 @@ describe('IndexPatternDimensionEditorPanel', () => { ); - expect(wrapper.find('[data-test-subj="lns-indexPatternDimension-derivative"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="lns-indexPatternDimension-differences"]')).toHaveLength( + 0 + ); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts deleted file mode 100644 index 82b6434e50aac..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ /dev/null @@ -1,1225 +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 { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; -import { IndexPatternDimensionEditorProps } from './dimension_panel'; -import { onDrop, getDropProps } from './droppable'; -import { DraggingIdentifier } from '../../drag_drop'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { IndexPatternPrivateState } from '../types'; -import { documentField } from '../document_field'; -import { OperationMetadata, DropType } from '../../types'; -import { IndexPatternColumn, MedianIndexPatternColumn } from '../operations'; -import { getFieldByNameFactory } from '../pure_helpers'; - -const fields = [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'src', - displayName: 'src', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, -]; - -const expectedIndexPatterns = { - foo: { - id: 'foo', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasExistence: true, - hasRestrictions: false, - fields, - getFieldByName: getFieldByNameFactory(fields), - }, -}; - -const defaultDragging = { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - humanData: { - label: 'Column 2', - }, -}; - -const draggingField = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, -}; - -/** - * The datasource exposes four main pieces of code which are tested at - * an integration test level. The main reason for this fairly high level - * of testing is that there is a lot of UI logic that isn't easily - * unit tested, such as the transient invalid state. - * - * - Dimension trigger: Not tested here - * - Dimension editor component: First half of the tests - * - * - getDropProps: Returns drop types that are possible for the current dragging field or other dimension - * - onDrop: Correct application of drop logic - */ -describe('IndexPatternDimensionEditorPanel', () => { - let state: IndexPatternPrivateState; - let setState: jest.Mock; - let defaultProps: IndexPatternDimensionEditorProps; - - beforeEach(() => { - state = { - indexPatternRefs: [], - indexPatterns: expectedIndexPatterns, - currentIndexPatternId: 'foo', - isFirstExistenceFetch: false, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Date histogram of timestamp', - customLabel: true, - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - }, - }, - incompleteColumns: {}, - }, - }, - }; - - setState = jest.fn(); - - defaultProps = { - state, - setState, - dateRange: { fromDate: 'now-1d', toDate: 'now' }, - columnId: 'col1', - layerId: 'first', - uniqueLabel: 'stuff', - groupId: 'group1', - filterOperations: () => true, - storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, - savedObjectsClient: {} as SavedObjectsClientContract, - http: {} as HttpSetup, - data: ({ - fieldFormats: ({ - getType: jest.fn().mockReturnValue({ - id: 'number', - title: 'Number', - }), - getDefaultType: jest.fn().mockReturnValue({ - id: 'bytes', - title: 'Bytes', - }), - } as unknown) as DataPublicPluginStart['fieldFormats'], - } as unknown) as DataPublicPluginStart, - core: {} as CoreSetup, - dimensionGroups: [], - }; - - jest.clearAllMocks(); - }); - - const groupId = 'a'; - - describe('getDropProps', () => { - it('returns undefined if no drag is happening', () => { - const dragging = { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }; - expect(getDropProps({ ...defaultProps, groupId, dragging })).toBe(undefined); - }); - - it('returns undefined if the dragged item has no field', () => { - const dragging = { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }; - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging, - }) - ).toBe(undefined); - }); - - it('returns undefined if field is not supported by filterOperations', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - id: 'mystring', - humanData: { label: 'Label' }, - }, - filterOperations: () => false, - }) - ).toBe(undefined); - }); - - it('returns remove_add if the field is supported by filterOperations and the dropTarget is an existing column', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' }); - }); - - it('returns undefined if the field belongs to another index pattern', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toBe(undefined); - }); - - it('returns undefined if the dragged field is already in use by this operation', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, - }, - }) - ).toBe(undefined); - }); - - it('returns move if the dragged column is compatible', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, - columnId: 'col2', - }) - ).toEqual({ dropType: 'move_compatible' }); - }); - - it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - - col2: { - label: 'Date histogram of timestamp (1)', - customLabel: true, - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, - - columnId: 'col2', - }) - ).toEqual(undefined); - }); - - it('returns replace_incompatible if dropping column to existing incompatible column', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ dropType: 'replace_incompatible', nextLabel: 'Unique count' }); - }); - }); - describe('onDrop', () => { - it('appends the dropped column when a field is dropped', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - dropType: 'field_replace', - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }, - }, - }, - }); - }); - - it('selects the specific operation that was valid on drop', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed, - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }, - }, - }, - }); - }); - - it('updates a column when a field is dropped', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }), - }), - }, - }); - }); - - it('keeps the operation when dropping a different compatible field', () => { - onDrop({ - ...defaultProps, - droppedItem: { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - }, - state: { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }, - }, - }, - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - operationType: 'sum', - dataType: 'number', - sourceField: 'memory', - }), - }), - }), - }, - }); - }); - - it('updates the column id when moving an operation to an empty dimension', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'bar', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: dragging, - columnId: 'col2', - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col2'], - columns: { - col2: state.layers.first.columns.col1, - }, - }, - }, - }); - }); - - it('replaces an operation when moving to a populated dimension', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - - col2: { - label: 'Top values of src', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - }, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', - sourceField: 'Records', - }, - }, - }; - - onDrop({ - ...defaultProps, - droppedItem: defaultDragging, - state: testState, - dropType: 'replace_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col3'], - columns: { - col1: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - }, - }, - }); - }); - - describe('dimension group aware ordering and copying', () => { - let dragging: DraggingIdentifier; - let testState: IndexPatternPrivateState; - beforeEach(() => { - dragging = { - columnId: 'col2', - groupId: 'b', - layerId: 'first', - id: 'col2', - humanData: { - label: '', - }, - }; - testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - col2: { - label: 'Top values of src', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - }, - col3: { - label: 'Top values of dest', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'dest', - }, - col4: { - label: 'Median of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'median', - sourceField: 'bytes', - }, - }, - }; - }); - const dimensionGroups = [ - { - accessors: [], - groupId: 'a', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: () => false, - }, - { - accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], - groupId: 'b', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: () => false, - }, - { - accessors: [{ columnId: 'col4' }], - groupId: 'c', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: () => false, - }, - ]; - - it('respects groups on moving operations from one group to another', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging col2 into newCol in group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - droppedItem: dragging, - state: testState, - groupId: 'a', - dimensionGroups, - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col3', 'col4'], - columns: { - newCol: testState.layers.first.columns.col2, - col1: testState.layers.first.columns.col1, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on moving operations from one group to another with overwrite', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // dragging col3 onto col1 in group a - const draggingCol3 = { - columnId: 'col3', - groupId: 'b', - layerId: 'first', - id: 'col3', - humanData: { - label: '', - }, - }; - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col4'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('moves newly created dimension to the bottom of the current group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col1 into newCol in group b - const draggingCol1 = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { - label: '', - }, - }; - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'move_compatible', - droppedItem: draggingCol1, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col2', 'col3', 'newCol', 'col4'], - columns: { - newCol: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('appends the dropped column in the right place when a field is dropped', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging field into newCol in group a - const draggingBytesField = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - humanData: { - label: '', - }, - }; - - onDrop({ - ...defaultProps, - droppedItem: draggingBytesField, - columnId: 'newCol', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - dimensionGroups, - dropType: 'field_add', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('appends the dropped column in the right place respecting custom nestingOrder', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging field into newCol in group a - const draggingBytesField = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - humanData: { - label: '', - }, - }; - - onDrop({ - ...defaultProps, - droppedItem: draggingBytesField, - columnId: 'newCol', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - dimensionGroups: [ - // a and b are ordered in reverse visually, but nesting order keeps them in place for column order - { ...dimensionGroups[1], nestingOrder: 1 }, - { ...dimensionGroups[0], nestingOrder: 0 }, - { ...dimensionGroups[2] }, - ], - dropType: 'field_add', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('copies column to the bottom of the current group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // copying col1 within group a - const draggingCol1 = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { - label: '', - }, - }; - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'duplicate_in_group', - droppedItem: draggingCol1, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('moves incompatible column to the bottom of the target group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into newCol in group a - const draggingCol4 = { - columnId: 'col4', - groupId: 'c', - layerId: 'first', - id: 'col4', - humanData: { - label: '', - }, - }; - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'move_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: expect.objectContaining({ - sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) - .sourceField, - }), - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - incompleteColumns: {}, - }, - }, - }); - }); - }); - - it('if dnd is reorder, it correctly reorders columns', () => { - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - }, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - operationType: 'terms', - sourceField: 'bar', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', - size: 5, - }, - }, - col3: { - operationType: 'avg', - sourceField: 'memory', - label: 'average of memory', - dataType: 'number', - isBucketed: false, - }, - }, - }, - }, - }; - - const metricDragging = { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: metricDragging, - state: testState, - dropType: 'duplicate_in_group', - columnId: 'newCol', - }); - // metric is appended - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'newCol'], - columns: { - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - newCol: testState.layers.first.columns.col3, - }, - }, - }, - }); - - const bucketDragging = { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: bucketDragging, - state: testState, - dropType: 'duplicate_in_group', - columnId: 'newCol', - }); - - // bucket is placed after the last existing bucket - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'newCol', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - newCol: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - }, - }, - }); - }); - - it('if dropType is reorder, it correctly reorders columns', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - const testState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - } as IndexPatternColumn, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - } as IndexPatternColumn, - col3: { - label: 'Top values of memory', - dataType: 'number', - isBucketed: true, - } as IndexPatternColumn, - }, - }, - }, - }; - - const defaultReorderDropParams = { - ...defaultProps, - dragging, - droppedItem: dragging, - state: testState, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'reorder' as DropType, - }; - - const stateWithColumnOrder = (columnOrder: string[]) => { - return { - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder, - columns: { - ...testState.layers.first.columns, - }, - }, - }, - }; - }; - - // first element to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); - - // last element to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - }, - }); - expect(setState).toBeCalledTimes(2); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); - - // middle column to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }, - }); - expect(setState).toBeCalledTimes(3); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); - - // middle column to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }, - }); - expect(setState).toBeCalledTimes(4); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts deleted file mode 100644 index e846db718f1d3..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ /dev/null @@ -1,386 +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 { - DatasourceDimensionDropProps, - DatasourceDimensionDropHandlerProps, - isDraggedOperation, - DraggedOperation, - DropType, -} from '../../types'; -import { IndexPatternColumn } from '../indexpattern'; -import { - insertOrReplaceColumn, - deleteColumn, - getOperationTypesForField, - getColumnOrder, - reorderByGroups, - getOperationDisplay, -} from '../operations'; -import { mergeLayer } from '../state_helpers'; -import { hasField, isDraggedField } from '../utils'; -import { IndexPatternPrivateState, DraggedField } from '../types'; -import { trackUiEvent } from '../../lens_ui_telemetry'; -import { DragContextState } from '../../drag_drop/providers'; - -type DropHandlerProps = DatasourceDimensionDropHandlerProps & { - droppedItem: T; -}; - -const operationLabels = getOperationDisplay(); - -export function getDropProps( - props: DatasourceDimensionDropProps & { - dragging: DragContextState['dragging']; - groupId: string; - } -): { dropType: DropType; nextLabel?: string } | undefined { - const { dragging } = props; - if (!dragging) { - return; - } - - const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; - - const currentColumn = props.state.layers[props.layerId].columns[props.columnId]; - if (isDraggedField(dragging)) { - const operationsForNewField = getOperationTypesForField(dragging.field, props.filterOperations); - - if (!!(layerIndexPatternId === dragging.indexPatternId && operationsForNewField.length)) { - const highestPriorityOperationLabel = operationLabels[operationsForNewField[0]].displayName; - if (!currentColumn) { - return { dropType: 'field_add', nextLabel: highestPriorityOperationLabel }; - } else if ( - (hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name) || - !hasField(currentColumn) - ) { - const persistingOperationLabel = - currentColumn && - operationsForNewField.includes(currentColumn.operationType) && - operationLabels[currentColumn.operationType].displayName; - - return { - dropType: 'field_replace', - nextLabel: persistingOperationLabel || highestPriorityOperationLabel, - }; - } - } - return; - } - - if ( - isDraggedOperation(dragging) && - dragging.layerId === props.layerId && - props.columnId !== dragging.columnId - ) { - // same group - if (props.groupId === dragging.groupId) { - if (currentColumn) { - return { dropType: 'reorder' }; - } - return { dropType: 'duplicate_in_group' }; - } - - // compatible group - const op = props.state.layers[dragging.layerId].columns[dragging.columnId]; - if ( - !op || - (currentColumn && - hasField(currentColumn) && - hasField(op) && - currentColumn.sourceField === op.sourceField) - ) { - return; - } - if (props.filterOperations(op)) { - if (currentColumn) { - return { dropType: 'replace_compatible' }; // in the future also 'swap_compatible' and 'duplicate_compatible' - } else { - return { dropType: 'move_compatible' }; // in the future also 'duplicate_compatible' - } - } - - // suggest - const field = - hasField(op) && props.state.indexPatterns[layerIndexPatternId].getFieldByName(op.sourceField); - const operationsForNewField = field && getOperationTypesForField(field, props.filterOperations); - - if (operationsForNewField && operationsForNewField?.length) { - const highestPriorityOperationLabel = operationLabels[operationsForNewField[0]].displayName; - - if (currentColumn) { - const persistingOperationLabel = - currentColumn && - operationsForNewField.includes(currentColumn.operationType) && - operationLabels[currentColumn.operationType].displayName; - return { - dropType: 'replace_incompatible', - nextLabel: persistingOperationLabel || highestPriorityOperationLabel, - }; // in the future also 'swap_incompatible', 'duplicate_incompatible' - } else { - return { - dropType: 'move_incompatible', - nextLabel: highestPriorityOperationLabel, - }; // in the future also 'duplicate_incompatible' - } - } - } -} - -export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const { droppedItem, dropType } = props; - - if (dropType === 'field_add' || dropType === 'field_replace') { - return operationOnDropMap[dropType]({ - ...props, - droppedItem: droppedItem as DraggedField, - }); - } - return operationOnDropMap[dropType]({ - ...props, - droppedItem: droppedItem as DraggedOperation, - }); -} - -const operationOnDropMap = { - field_add: onFieldDrop, - field_replace: onFieldDrop, - reorder: onReorderDrop, - duplicate_in_group: onSameGroupDuplicateDrop, - move_compatible: onMoveDropToCompatibleGroup, - replace_compatible: onMoveDropToCompatibleGroup, - move_incompatible: onMoveDropToNonCompatibleGroup, - replace_incompatible: onMoveDropToNonCompatibleGroup, -}; - -function reorderElements(items: string[], dest: string, src: string) { - const result = items.filter((c) => c !== src); - const destIndex = items.findIndex((c) => c === src); - const destPosition = result.indexOf(dest); - - const srcIndex = items.findIndex((c) => c === dest); - - result.splice(destIndex < srcIndex ? destPosition + 1 : destPosition, 0, src); - return result; -} - -function onReorderDrop({ - columnId, - setState, - state, - layerId, - droppedItem, -}: DropHandlerProps) { - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: reorderElements( - state.layers[layerId].columnOrder, - columnId, - droppedItem.columnId - ), - }, - }) - ); - - return true; -} - -function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { - const { columnId, setState, state, layerId, droppedItem, dimensionGroups, groupId } = props; - - const layer = state.layers[layerId]; - const op = { ...layer.columns[droppedItem.columnId] }; - const field = - hasField(op) && state.indexPatterns[layer.indexPatternId].getFieldByName(op.sourceField); - if (!field) { - return false; - } - - const operationsForNewField = getOperationTypesForField(field, props.filterOperations); - - if (!operationsForNewField.length) { - return false; - } - - const currentIndexPattern = state.indexPatterns[layer.indexPatternId]; - // Detects if we can change the field only, otherwise change field + operation - - const selectedColumn: IndexPatternColumn | null = layer.columns[columnId] || null; - - const fieldIsCompatibleWithCurrent = - selectedColumn && operationsForNewField.includes(selectedColumn.operationType); - - const newLayer = insertOrReplaceColumn({ - layer: deleteColumn({ - layer, - columnId: droppedItem.columnId, - indexPattern: currentIndexPattern, - }), - columnId, - indexPattern: currentIndexPattern, - op: fieldIsCompatibleWithCurrent ? selectedColumn.operationType : operationsForNewField[0], - field, - visualizationGroups: dimensionGroups, - targetGroup: groupId, - }); - - trackUiEvent('drop_onto_dimension'); - setState( - mergeLayer({ - state, - layerId, - newLayer: { - ...newLayer, - }, - }) - ); - - return { deleted: droppedItem.columnId }; -} - -function onSameGroupDuplicateDrop({ - columnId, - setState, - state, - layerId, - droppedItem, - dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - - const op = { ...layer.columns[droppedItem.columnId] }; - const newColumns = { - ...layer.columns, - [columnId]: op, - }; - - const newColumnOrder = [...layer.columnOrder]; - // put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array - // then reorder based on dimension groups if necessary - const insertionIndex = op.isBucketed - ? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed) - : newColumnOrder.length; - newColumnOrder.splice(insertionIndex, 0, columnId); - - const newLayer = { - ...layer, - columnOrder: newColumnOrder, - columns: newColumns, - }; - - const updatedColumnOrder = getColumnOrder(newLayer); - - reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); - - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: updatedColumnOrder, - columns: newColumns, - }, - }) - ); - return true; -} - -function onMoveDropToCompatibleGroup({ - columnId, - setState, - state, - layerId, - droppedItem, - dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const op = { ...layer.columns[droppedItem.columnId] }; - const newColumns = { ...layer.columns }; - delete newColumns[droppedItem.columnId]; - newColumns[columnId] = op; - - const newColumnOrder = [...layer.columnOrder]; - const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); - const newIndex = newColumnOrder.findIndex((c) => c === columnId); - - if (newIndex === -1) { - // for newly created columns, remove the old entry and add the last one to the end - newColumnOrder.splice(oldIndex, 1); - newColumnOrder.push(columnId); - } else { - // for drop to replace, reuse the same index - newColumnOrder[oldIndex] = columnId; - } - const newLayer = { - ...layer, - columnOrder: newColumnOrder, - columns: newColumns, - }; - - const updatedColumnOrder = getColumnOrder(newLayer); - - reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); - - // Time to replace - setState( - mergeLayer({ - state, - layerId, - - newLayer: { - columnOrder: updatedColumnOrder, - columns: newColumns, - }, - }) - ); - return { deleted: droppedItem.columnId }; -} - -function onFieldDrop(props: DropHandlerProps) { - const { columnId, setState, state, layerId, droppedItem, groupId, dimensionGroups } = props; - - const operationsForNewField = getOperationTypesForField( - droppedItem.field, - props.filterOperations - ); - - if (!isDraggedField(droppedItem) || !operationsForNewField.length) { - // TODO: What do we do if we couldn't find a column? - return false; - } - - const layer = state.layers[layerId]; - - const selectedColumn: IndexPatternColumn | null = layer.columns[columnId] || null; - const currentIndexPattern = state.indexPatterns[layer.indexPatternId]; - - // Detects if we can change the field only, otherwise change field + operation - const fieldIsCompatibleWithCurrent = - selectedColumn && operationsForNewField.includes(selectedColumn.operationType); - - const newLayer = insertOrReplaceColumn({ - layer, - columnId, - indexPattern: currentIndexPattern, - op: fieldIsCompatibleWithCurrent ? selectedColumn.operationType : operationsForNewField[0], - field: droppedItem.field, - visualizationGroups: dimensionGroups, - targetGroup: groupId, - }); - - trackUiEvent('drop_onto_dimension'); - const hasData = Object.values(state.layers).some(({ columns }) => columns.length); - trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - setState(mergeLayer({ state, layerId, newLayer })); - return true; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts new file mode 100644 index 0000000000000..051feb331aec4 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -0,0 +1,1556 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; +import { IndexPatternDimensionEditorProps } from '../dimension_panel'; +import { onDrop } from './on_drop_handler'; +import { getDropProps } from './get_drop_props'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { IndexPatternLayer, IndexPatternPrivateState } from '../../types'; +import { documentField } from '../../document_field'; +import { OperationMetadata, DropType } from '../../../types'; +import { IndexPatternColumn, MedianIndexPatternColumn } from '../../operations'; +import { getFieldByNameFactory } from '../../pure_helpers'; + +const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'src', + displayName: 'src', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, +]; + +const expectedIndexPatterns = { + foo: { + id: 'foo', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasExistence: true, + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }, +}; + +const dimensionGroups = [ + { + accessors: [], + groupId: 'a', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], + groupId: 'b', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + accessors: [{ columnId: 'col4' }], + groupId: 'c', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, +]; + +const oneColumnLayer: IndexPatternLayer = { + indexPatternId: 'foo', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Date histogram of timestamp', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, +}; + +const multipleColumnsLayer: IndexPatternLayer = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: oneColumnLayer.columns.col1, + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Top values of dest', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'dest', + }, + col4: { + label: 'Median of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'median', + sourceField: 'bytes', + }, + }, +}; + +const draggingField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, +}; + +const draggingCol1 = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Column 1' }, +}; + +const draggingCol2 = { + columnId: 'col2', + groupId: 'b', + layerId: 'first', + id: 'col2', + humanData: { label: 'Column 2' }, + filterOperations: (op: OperationMetadata) => op.isBucketed, +}; + +const draggingCol3 = { + columnId: 'col3', + groupId: 'b', + layerId: 'first', + id: 'col3', + humanData: { + label: '', + }, +}; + +const draggingCol4 = { + columnId: 'col4', + groupId: 'c', + layerId: 'first', + id: 'col4', + humanData: { + label: '', + }, + filterOperations: (op: OperationMetadata) => op.isBucketed === false, +}; + +/** + * The datasource exposes four main pieces of code which are tested at + * an integration test level. The main reason for this fairly high level + * of testing is that there is a lot of UI logic that isn't easily + * unit tested, such as the transient invalid state. + * + * - Dimension trigger: Not tested here + * - Dimension editor component: First half of the tests + * + * - getDropProps: Returns drop types that are possible for the current dragging field or other dimension + * - onDrop: Correct application of drop logic + */ +describe('IndexPatternDimensionEditorPanel', () => { + let state: IndexPatternPrivateState; + let setState: jest.Mock; + let defaultProps: IndexPatternDimensionEditorProps; + + beforeEach(() => { + state = { + indexPatternRefs: [], + indexPatterns: expectedIndexPatterns, + currentIndexPatternId: 'foo', + isFirstExistenceFetch: false, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { first: { ...oneColumnLayer } }, + }; + + setState = jest.fn(); + + defaultProps = { + state, + setState, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + columnId: 'col1', + layerId: 'first', + uniqueLabel: 'stuff', + groupId: 'group1', + filterOperations: () => true, + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + http: {} as HttpSetup, + data: ({ + fieldFormats: ({ + getType: jest.fn().mockReturnValue({ + id: 'number', + title: 'Number', + }), + getDefaultType: jest.fn().mockReturnValue({ + id: 'bytes', + title: 'Bytes', + }), + } as unknown) as DataPublicPluginStart['fieldFormats'], + } as unknown) as DataPublicPluginStart, + core: {} as CoreSetup, + dimensionGroups: [], + }; + + jest.clearAllMocks(); + }); + + const groupId = 'a'; + + describe('getDropProps', () => { + it('returns undefined if no drag is happening', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, + }) + ).toBe(undefined); + }); + + it('returns undefined if the dragged item has no field', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }, + }) + ).toBe(undefined); + }); + + describe('dragging a field', () => { + it('returns undefined if field is not supported by filterOperations', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: draggingField, + filterOperations: () => false, + }) + ).toBe(undefined); + }); + + it('returns field_replace if the field is supported by filterOperations and the dropTarget is an existing column', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: draggingField, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }) + ).toEqual({ dropTypes: ['field_replace'], nextLabel: 'Intervals' }); + }); + + it('returns field_add if the field is supported by filterOperations and the dropTarget is an empty column', () => { + expect( + getDropProps({ + ...defaultProps, + columnId: 'newId', + groupId, + dragging: draggingField, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }) + ).toEqual({ dropTypes: ['field_add'], nextLabel: 'Intervals' }); + }); + + it('returns undefined if the field belongs to another index pattern', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + id: 'bar', + humanData: { label: 'Label' }, + }, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }) + ).toBe(undefined); + }); + + it('returns undefined if the dragged field is already in use by this operation', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, + }, + }) + ).toBe(undefined); + }); + }); + + describe('dragging a column', () => { + it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Date histogram of timestamp (1)', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + }, + }; + + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + ...draggingCol1, + groupId: 'c', + }, + columnId: 'col2', + }) + ).toEqual(undefined); + }); + + it('returns reorder if drop target and droppedItem columns are from the same group and both are existing', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'sum', + sourceField: 'bytes', + }, + }, + }; + + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { ...draggingCol1, groupId }, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }) + ).toEqual({ + dropTypes: ['reorder'], + }); + }); + + it('returns duplicate_compatible if drop target and droppedItem columns are from the same group and drop target id is a new column', () => { + expect( + getDropProps({ + ...defaultProps, + columnId: 'newId', + groupId, + dragging: { + ...draggingCol1, + groupId, + }, + }) + ).toEqual({ dropTypes: ['duplicate_compatible'] }); + }); + + it('returns compatible drop types if the dragged column is compatible', () => { + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + ...draggingCol1, + groupId: 'c', + }, + columnId: 'col2', + }) + ).toEqual({ dropTypes: ['move_compatible', 'duplicate_compatible'] }); + }); + + it('returns incompatible drop target types if dropping column to existing incompatible column', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'sum', + sourceField: 'bytes', + }, + }, + }; + + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + ...draggingCol1, + groupId: 'c', + }, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }) + ).toEqual({ + dropTypes: [ + 'replace_incompatible', + 'replace_duplicate_incompatible', + 'swap_incompatible', + ], + nextLabel: 'Unique count', + }); + }); + + it('does not return swap_incompatible if current dropTarget column cannot be swapped to the group of dragging column', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }; + + expect( + getDropProps({ + ...defaultProps, + groupId, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => op.isBucketed === true, + }, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }) + ).toEqual({ + dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible'], + nextLabel: 'Unique count', + }); + }); + }); + }); + + describe('onDrop', () => { + describe('dropping a field', () => { + it('updates a column when a field is dropped', () => { + onDrop({ + ...defaultProps, + droppedItem: draggingField, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }), + }), + }, + }); + }); + it('selects the specific operation that was valid on drop', () => { + onDrop({ + ...defaultProps, + droppedItem: draggingField, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }, + }, + }, + }); + }); + it('keeps the operation when dropping a different compatible field', () => { + onDrop({ + ...defaultProps, + droppedItem: { + field: { name: 'memory', type: 'number', aggregatable: true }, + indexPatternId: 'foo', + id: '1', + }, + state: { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'sum', + sourceField: 'bytes', + }, + }, + }, + }, + }, + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + operationType: 'sum', + dataType: 'number', + sourceField: 'memory', + }), + }), + }), + }, + }); + }); + it('appends the dropped column when a field is dropped', () => { + onDrop({ + ...defaultProps, + droppedItem: draggingField, + dropType: 'field_replace', + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }, + }, + }, + }); + }); + it('dimensionGroups are defined - appends the dropped column in the right place when a field is dropped', () => { + const testState = { ...state }; + testState.layers.first = { ...multipleColumnsLayer }; + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + + onDrop({ + ...defaultProps, + droppedItem: draggingField, + columnId: 'newCol', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + dimensionGroups, + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + }); + + describe('dropping a dimension', () => { + const dragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + + it('sets correct order in group for metric and bucket columns when duplicating a column in group', () => { + const testState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + col2: { + label: 'Top values of bar', + dataType: 'number', + isBucketed: true, + operationType: 'terms', + sourceField: 'bar', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }, + col3: { + operationType: 'average', + sourceField: 'memory', + label: 'average of memory', + dataType: 'number', + isBucketed: false, + }, + }, + }, + }, + }; + + const metricDragging = { + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + droppedItem: metricDragging, + state: testState, + dropType: 'duplicate_compatible', + columnId: 'newCol', + }); + // metric is appended + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'newCol'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + newCol: testState.layers.first.columns.col3, + }, + }, + }, + }); + + const bucketDragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + droppedItem: bucketDragging, + state: testState, + dropType: 'duplicate_compatible', + columnId: 'newCol', + }); + + // bucket is placed after the last existing bucket + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'newCol', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + newCol: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); + }); + + it('sets correct order in group when reordering a column in group', () => { + const testState = { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + } as IndexPatternColumn, + col2: { + label: 'Top values of bar', + dataType: 'number', + isBucketed: true, + } as IndexPatternColumn, + col3: { + label: 'Top values of memory', + dataType: 'number', + isBucketed: true, + } as IndexPatternColumn, + }, + }, + }, + }; + + const defaultReorderDropParams = { + ...defaultProps, + dragging, + droppedItem: draggingCol1, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + dropType: 'reorder' as DropType, + }; + + const stateWithColumnOrder = (columnOrder: string[]) => { + return { + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder, + columns: { + ...testState.layers.first.columns, + }, + }, + }, + }; + }; + + // first element to last + onDrop({ + ...defaultReorderDropParams, + columnId: 'col3', + }); + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); + + // last element to first + onDrop({ + ...defaultReorderDropParams, + columnId: 'col1', + droppedItem: { + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + }, + }); + expect(setState).toBeCalledTimes(2); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); + + // middle column to first + onDrop({ + ...defaultReorderDropParams, + columnId: 'col1', + droppedItem: { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(3); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); + + // middle column to last + onDrop({ + ...defaultReorderDropParams, + columnId: 'col3', + droppedItem: { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(4); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); + }); + + it('updates the column id when moving an operation to an empty dimension', () => { + onDrop({ + ...defaultProps, + droppedItem: draggingCol1, + columnId: 'col2', + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columnOrder: ['col2'], + columns: { + col2: state.layers.first.columns.col1, + }, + }, + }, + }); + }); + + it('replaces an operation when moving to a populated dimension', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col3' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', + }, + }, + }; + + onDrop({ + ...defaultProps, + droppedItem: draggingCol2, + state: testState, + dropType: 'replace_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); + }); + + describe('dimension group aware ordering and copying', () => { + let testState: IndexPatternPrivateState; + beforeEach(() => { + testState = { ...state }; + testState.layers.first = { ...multipleColumnsLayer }; + }); + + it('respects groups on moving operations between compatible groups', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + columnId: 'newCol', + droppedItem: draggingCol2, + state: testState, + groupId: 'a', + dimensionGroups, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on duplicating operations between compatible groups', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + columnId: 'newCol', + droppedItem: draggingCol2, + state: testState, + groupId: 'a', + dimensionGroups, + dropType: 'duplicate_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations between compatible groups with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + onDrop({ + ...defaultProps, + columnId: 'col1', + droppedItem: draggingCol3, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on duplicating operations between compatible groups with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + + onDrop({ + ...defaultProps, + columnId: 'col1', + droppedItem: draggingCol3, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'duplicate_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves newly created dimension to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col1 into newCol in group b + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'move_compatible', + droppedItem: draggingCol1, + state: testState, + groupId: 'b', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col2', 'col3', 'newCol', 'col4'], + columns: { + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('copies column to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // copying col1 within group a + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'duplicate_compatible', + droppedItem: draggingCol1, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('appends the dropped column in the right place respecting custom nestingOrder', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + + onDrop({ + ...defaultProps, + droppedItem: draggingField, + columnId: 'newCol', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + dimensionGroups: [ + // a and b are ordered in reverse visually, but nesting order keeps them in place for column order + { ...dimensionGroups[1], nestingOrder: 1 }, + { ...dimensionGroups[0], nestingOrder: 0 }, + { ...dimensionGroups[2] }, + ], + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('moves incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'move_incompatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('copies incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'duplicate_incompatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('moves incompatible column with overwrite keeping order of target column', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col2 in group b + + onDrop({ + ...defaultProps, + columnId: 'col2', + dropType: 'move_incompatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'b', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + isBucketed: true, + label: 'Top values of bytes', + operationType: 'terms', + sourceField: 'bytes', + dataType: 'number', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + size: 10, + }, + }, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('when swapping compatibly, columns carry order', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col1 + + onDrop({ + ...defaultProps, + columnId: 'col1', + dropType: 'swap_compatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col4, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col1, + }, + }, + }, + }); + }); + + it('when swapping incompatibly, newly created columns take order from the columns they replace', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col2 + + onDrop({ + ...defaultProps, + columnId: 'col2', + dropType: 'swap_incompatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'b', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + isBucketed: true, + label: 'Top values of bytes', + operationType: 'terms', + sourceField: 'bytes', + dataType: 'number', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + size: 10, + }, + }, + col3: testState.layers.first.columns.col3, + col4: { + isBucketed: false, + label: 'Unique count of src', + filter: undefined, + operationType: 'unique_count', + sourceField: 'src', + dataType: 'number', + params: undefined, + scale: 'ratio', + }, + }, + incompleteColumns: {}, + }, + }, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts new file mode 100644 index 0000000000000..a98a29aea6682 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { + DatasourceDimensionDropProps, + isDraggedOperation, + DraggedOperation, + DropType, +} from '../../../types'; +import { getOperationDisplay } from '../../operations'; +import { hasField, isDraggedField } from '../../utils'; +import { DragContextState } from '../../../drag_drop/providers'; +import { OperationMetadata } from '../../../types'; +import { getOperationTypesForField } from '../../operations'; +import { IndexPatternColumn } from '../../indexpattern'; +import { + IndexPatternPrivateState, + IndexPattern, + IndexPatternField, + DraggedField, +} from '../../types'; + +type GetDropProps = DatasourceDimensionDropProps & { + dragging?: DragContextState['dragging']; + groupId: string; +}; + +type DropProps = { dropTypes: DropType[]; nextLabel?: string } | undefined; + +const operationLabels = getOperationDisplay(); + +export function getNewOperation( + field: IndexPatternField | undefined | false, + filterOperations: (meta: OperationMetadata) => boolean, + targetColumn: IndexPatternColumn +) { + if (!field) { + return; + } + const newOperations = getOperationTypesForField(field, filterOperations); + if (!newOperations.length) { + return; + } + // Detects if we can change the field only, otherwise change field + operation + const shouldOperationPersist = targetColumn && newOperations.includes(targetColumn.operationType); + return shouldOperationPersist ? targetColumn.operationType : newOperations[0]; +} + +export function getField(column: IndexPatternColumn | undefined, indexPattern: IndexPattern) { + if (!column) { + return; + } + const field = (hasField(column) && indexPattern.getFieldByName(column.sourceField)) || undefined; + return field; +} + +export function getDropProps(props: GetDropProps) { + const { state, columnId, layerId, dragging, groupId, filterOperations } = props; + if (!dragging) { + return; + } + + if (isDraggedField(dragging)) { + return getDropPropsForField({ ...props, dragging }); + } + + if ( + isDraggedOperation(dragging) && + dragging.layerId === layerId && + columnId !== dragging.columnId + ) { + const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; + const targetColumn = state.layers[layerId].columns[columnId]; + + const isSameGroup = groupId === dragging.groupId; + if (isSameGroup) { + return getDropPropsForSameGroup(targetColumn); + } else if (hasTheSameField(sourceColumn, targetColumn)) { + return; + } else if (filterOperations(sourceColumn)) { + return getDropPropsForCompatibleGroup(targetColumn); + } else { + return getDropPropsFromIncompatibleGroup({ ...props, dragging }); + } + } +} + +function hasTheSameField(sourceColumn: IndexPatternColumn, targetColumn?: IndexPatternColumn) { + return ( + targetColumn && + hasField(targetColumn) && + hasField(sourceColumn) && + targetColumn.sourceField === sourceColumn.sourceField + ); +} + +function getDropPropsForField({ + state, + columnId, + layerId, + dragging, + filterOperations, +}: GetDropProps & { dragging: DraggedField }): DropProps { + const targetColumn = state.layers[layerId].columns[columnId]; + const isTheSameIndexPattern = state.layers[layerId].indexPatternId === dragging.indexPatternId; + const newOperation = getNewOperation(dragging.field, filterOperations, targetColumn); + + if (!!(isTheSameIndexPattern && newOperation)) { + const nextLabel = operationLabels[newOperation].displayName; + + if (!targetColumn) { + return { dropTypes: ['field_add'], nextLabel }; + } else if ( + (hasField(targetColumn) && targetColumn.sourceField !== dragging.field.name) || + !hasField(targetColumn) + ) { + return { + dropTypes: ['field_replace'], + nextLabel, + }; + } + } + return; +} + +function getDropPropsForSameGroup(targetColumn?: IndexPatternColumn): DropProps { + return targetColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; +} + +function getDropPropsForCompatibleGroup(targetColumn?: IndexPatternColumn): DropProps { + return { + dropTypes: targetColumn + ? ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'] + : ['move_compatible', 'duplicate_compatible'], + }; +} + +function getDropPropsFromIncompatibleGroup({ + state, + columnId, + layerId, + dragging, + filterOperations, +}: GetDropProps & { dragging: DraggedOperation }): DropProps { + const targetColumn = state.layers[layerId].columns[columnId]; + const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; + + const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + const sourceField = getField(sourceColumn, layerIndexPattern); + const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn); + + if (newOperationForSource) { + const targetField = getField(targetColumn, layerIndexPattern); + const canSwap = !!getNewOperation(targetField, dragging.filterOperations, sourceColumn); + + return { + dropTypes: targetColumn + ? canSwap + ? ['replace_incompatible', 'replace_duplicate_incompatible', 'swap_incompatible'] + : ['replace_incompatible', 'replace_duplicate_incompatible'] + : ['move_incompatible', 'duplicate_incompatible'], + nextLabel: operationLabels[newOperationForSource].displayName, + }; + } +} diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/index.ts similarity index 69% rename from x-pack/plugins/data_enhanced/public/autocomplete/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/index.ts index 7910ce3ffb237..07adce49eb90a 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/index.ts @@ -5,7 +5,5 @@ * 2.0. */ -export { - setupKqlQuerySuggestionProvider, - KUERY_LANGUAGE_NAME, -} from './providers/kql_query_suggestion'; +export { onDrop } from './on_drop_handler'; +export { getDropProps } from './get_drop_props'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts new file mode 100644 index 0000000000000..17b5cbc661ca3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -0,0 +1,358 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { DatasourceDimensionDropHandlerProps, DraggedOperation } from '../../../types'; +import { + insertOrReplaceColumn, + deleteColumn, + getColumnOrder, + reorderByGroups, +} from '../../operations'; +import { mergeLayer } from '../../state_helpers'; +import { isDraggedField } from '../../utils'; +import { getNewOperation, getField } from './get_drop_props'; +import { IndexPatternPrivateState, DraggedField } from '../../types'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; + +type DropHandlerProps = DatasourceDimensionDropHandlerProps & { + droppedItem: T; +}; + +export function onDrop(props: DatasourceDimensionDropHandlerProps) { + const { droppedItem, dropType } = props; + + if (dropType === 'field_add' || dropType === 'field_replace') { + return operationOnDropMap[dropType]({ + ...props, + droppedItem: droppedItem as DraggedField, + }); + } + return operationOnDropMap[dropType]({ + ...props, + droppedItem: droppedItem as DraggedOperation, + }); +} + +const operationOnDropMap = { + field_add: onFieldDrop, + field_replace: onFieldDrop, + + reorder: onReorder, + + move_compatible: (props: DropHandlerProps) => onMoveCompatible(props, true), + replace_compatible: (props: DropHandlerProps) => onMoveCompatible(props, true), + duplicate_compatible: onMoveCompatible, + replace_duplicate_compatible: onMoveCompatible, + + move_incompatible: (props: DropHandlerProps) => onMoveIncompatible(props, true), + replace_incompatible: (props: DropHandlerProps) => + onMoveIncompatible(props, true), + duplicate_incompatible: onMoveIncompatible, + replace_duplicate_incompatible: onMoveIncompatible, + + swap_compatible: onSwapCompatible, + swap_incompatible: onSwapIncompatible, +}; + +function onFieldDrop(props: DropHandlerProps) { + const { + columnId, + setState, + state, + layerId, + droppedItem, + filterOperations, + groupId, + dimensionGroups, + } = props; + + const layer = state.layers[layerId]; + const indexPattern = state.indexPatterns[layer.indexPatternId]; + const targetColumn = layer.columns[columnId]; + const newOperation = getNewOperation(droppedItem.field, filterOperations, targetColumn); + + if (!isDraggedField(droppedItem) || !newOperation) { + return false; + } + + const newLayer = insertOrReplaceColumn({ + layer, + columnId, + indexPattern, + op: newOperation, + field: droppedItem.field, + visualizationGroups: dimensionGroups, + targetGroup: groupId, + }); + + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + setState(mergeLayer({ state, layerId, newLayer })); + return true; +} + +function onMoveCompatible( + { + columnId, + setState, + state, + layerId, + droppedItem, + dimensionGroups, + groupId, + }: DropHandlerProps, + shouldDeleteSource?: boolean +) { + const layer = state.layers[layerId]; + const sourceColumn = layer.columns[droppedItem.columnId]; + + const newColumns = { + ...layer.columns, + [columnId]: { ...sourceColumn }, + }; + if (shouldDeleteSource) { + delete newColumns[droppedItem.columnId]; + } + + const newColumnOrder = [...layer.columnOrder]; + + if (shouldDeleteSource) { + const sourceIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); + const targetIndex = newColumnOrder.findIndex((c) => c === columnId); + + if (targetIndex === -1) { + // for newly created columns, remove the old entry and add the last one to the end + newColumnOrder.splice(sourceIndex, 1); + newColumnOrder.push(columnId); + } else { + // for drop to replace, reuse the same index + newColumnOrder[sourceIndex] = columnId; + } + } else { + // put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array + // then reorder based on dimension groups if necessary + const insertionIndex = sourceColumn.isBucketed + ? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed) + : newColumnOrder.length; + newColumnOrder.splice(insertionIndex, 0, columnId); + } + + const newLayer = { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }; + + const updatedColumnOrder = getColumnOrder(newLayer); + + reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); + + // Time to replace + setState( + mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: updatedColumnOrder, + columns: newColumns, + }, + }) + ); + return shouldDeleteSource ? { deleted: droppedItem.columnId } : true; +} + +function onReorder({ + columnId, + setState, + state, + layerId, + droppedItem, +}: DropHandlerProps) { + function reorderElements(items: string[], dest: string, src: string) { + const result = items.filter((c) => c !== src); + const targetIndex = items.findIndex((c) => c === src); + const sourceIndex = items.findIndex((c) => c === dest); + + const targetPosition = result.indexOf(dest); + result.splice(targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, 0, src); + return result; + } + + setState( + mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: reorderElements( + state.layers[layerId].columnOrder, + columnId, + droppedItem.columnId + ), + }, + }) + ); + return true; +} + +function onMoveIncompatible( + { + columnId, + setState, + state, + layerId, + droppedItem, + filterOperations, + dimensionGroups, + groupId, + }: DropHandlerProps, + shouldDeleteSource?: boolean +) { + const layer = state.layers[layerId]; + const indexPattern = state.indexPatterns[layer.indexPatternId]; + const sourceColumn = layer.columns[droppedItem.columnId]; + const targetColumn = layer.columns[columnId] || null; + + const sourceField = getField(sourceColumn, indexPattern); + const newOperation = getNewOperation(sourceField, filterOperations, targetColumn); + if (!newOperation) { + return false; + } + + const modifiedLayer = shouldDeleteSource + ? deleteColumn({ + layer, + columnId: droppedItem.columnId, + indexPattern, + }) + : layer; + + const newLayer = insertOrReplaceColumn({ + layer: modifiedLayer, + columnId, + indexPattern, + op: newOperation, + field: sourceField, + visualizationGroups: dimensionGroups, + targetGroup: groupId, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId, + newLayer, + }) + ); + return shouldDeleteSource ? { deleted: droppedItem.columnId } : true; +} + +function onSwapIncompatible({ + columnId, + setState, + state, + layerId, + droppedItem, + filterOperations, + dimensionGroups, + groupId, +}: DropHandlerProps) { + const layer = state.layers[layerId]; + const indexPattern = state.indexPatterns[layer.indexPatternId]; + const sourceColumn = layer.columns[droppedItem.columnId]; + const targetColumn = layer.columns[columnId]; + + const sourceField = getField(sourceColumn, indexPattern); + const targetField = getField(targetColumn, indexPattern); + + const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn); + const newOperationForTarget = getNewOperation( + targetField, + droppedItem.filterOperations, + sourceColumn + ); + + if (!newOperationForSource || !newOperationForTarget) { + return false; + } + + const newLayer = insertOrReplaceColumn({ + layer: insertOrReplaceColumn({ + layer, + columnId, + targetGroup: groupId, + indexPattern, + op: newOperationForSource, + field: sourceField, + visualizationGroups: dimensionGroups, + }), + columnId: droppedItem.columnId, + indexPattern, + op: newOperationForTarget, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: droppedItem.groupId, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId, + newLayer, + }) + ); + return true; +} + +const swapColumnOrder = (columnOrder: string[], sourceId: string, targetId: string) => { + const newColumnOrder = [...columnOrder]; + const sourceIndex = newColumnOrder.findIndex((c) => c === sourceId); + const targetIndex = newColumnOrder.findIndex((c) => c === targetId); + + newColumnOrder[sourceIndex] = targetId; + newColumnOrder[targetIndex] = sourceId; + + return newColumnOrder; +}; + +function onSwapCompatible({ + columnId, + setState, + state, + layerId, + droppedItem, + dimensionGroups, + groupId, +}: DropHandlerProps) { + const layer = state.layers[layerId]; + const sourceId = droppedItem.columnId; + const targetId = columnId; + + const sourceColumn = { ...layer.columns[sourceId] }; + const targetColumn = { ...layer.columns[targetId] }; + const newColumns = { ...layer.columns }; + newColumns[targetId] = sourceColumn; + newColumns[sourceId] = targetColumn; + + const updatedColumnOrder = swapColumnOrder(layer.columnOrder, sourceId, targetId); + reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); + + // Time to replace + setState( + mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: updatedColumnOrder, + columns: newColumns, + }, + }) + ); + + return true; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index f44004e14d580..ffd4ac2498133 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -32,7 +32,7 @@ export interface FieldChoice { operationType: OperationType; } -export interface FieldSelectProps extends EuiComboBoxProps<{}> { +export interface FieldSelectProps extends EuiComboBoxProps { currentIndexPattern: IndexPattern; selectedOperationType?: OperationType; selectedField?: string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index 9ad6a2d20a4c2..f17adf9be39f3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -157,7 +157,7 @@ describe('reference editor', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -192,7 +192,7 @@ describe('reference editor', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -233,7 +233,7 @@ describe('reference editor', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -276,7 +276,7 @@ describe('reference editor', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -296,9 +296,9 @@ describe('reference editor', () => { expect(subFunctionSelect.prop('selectedOptions')).toEqual( expect.arrayContaining([ expect.objectContaining({ - 'data-test-subj': 'lns-indexPatternDimension-avg incompatible', + 'data-test-subj': 'lns-indexPatternDimension-average incompatible', label: 'Average', - value: 'avg', + value: 'average', }), ]) ); @@ -334,7 +334,7 @@ describe('reference editor', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -352,7 +352,7 @@ describe('reference editor', () => { const fieldSelect = wrapper.find(FieldSelect); expect(fieldSelect.prop('fieldIsInvalid')).toEqual(true); expect(fieldSelect.prop('selectedField')).toEqual('bytes'); - expect(fieldSelect.prop('selectedOperationType')).toEqual('avg'); + expect(fieldSelect.prop('selectedOperationType')).toEqual('average'); expect(fieldSelect.prop('incompleteOperation')).toEqual('max'); expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(false); }); @@ -369,7 +369,7 @@ describe('reference editor', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -387,7 +387,7 @@ describe('reference editor', () => { const fieldSelect = wrapper.find(FieldSelect); expect(fieldSelect.prop('fieldIsInvalid')).toEqual(false); expect(fieldSelect.prop('selectedField')).toEqual('timestamp'); - expect(fieldSelect.prop('selectedOperationType')).toEqual('avg'); + expect(fieldSelect.prop('selectedOperationType')).toEqual('average'); expect(fieldSelect.prop('incompleteOperation')).toBeUndefined(); }); @@ -423,7 +423,7 @@ describe('reference editor', () => { label: 'Average of missing', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'missing', }, }, @@ -438,7 +438,7 @@ describe('reference editor', () => { const fieldSelect = wrapper.find(FieldSelect); expect(fieldSelect.prop('fieldIsInvalid')).toEqual(true); expect(fieldSelect.prop('selectedField')).toEqual('missing'); - expect(fieldSelect.prop('selectedOperationType')).toEqual('avg'); + expect(fieldSelect.prop('selectedOperationType')).toEqual('average'); expect(fieldSelect.prop('incompleteOperation')).toBeUndefined(); expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(false); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index c02515c2e7201..14834adfc33cc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -546,7 +546,7 @@ describe('IndexPattern Data Source', () => { dataType: 'number', isBucketed: false, sourceField: 'bytes', - operationType: 'avg', + operationType: 'average', timeScale: 'h', }, col3: { @@ -659,7 +659,7 @@ describe('IndexPattern Data Source', () => { dataType: 'number', isBucketed: false, sourceField: 'bytes', - operationType: 'avg', + operationType: 'average', timeScale: 'h', }, col3: { @@ -835,7 +835,7 @@ describe('IndexPattern Data Source', () => { dataType: 'date', isBucketed: false, sourceField: 'timefield', - operationType: 'cardinality', + operationType: 'unique_count', }, col2: { label: 'Date', @@ -885,7 +885,7 @@ describe('IndexPattern Data Source', () => { dataType: 'date', isBucketed: false, sourceField: 'timefield', - operationType: 'cardinality', + operationType: 'unique_count', }, col2: { label: 'Reference', @@ -1183,7 +1183,7 @@ describe('IndexPattern Data Source', () => { dataType: 'number', isBucketed: false, label: 'Foo', - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -1210,7 +1210,7 @@ describe('IndexPattern Data Source', () => { columnOrder: [], columns: {}, incompleteColumns: { - col1: { operationType: 'avg' as const }, + col1: { operationType: 'average' as const }, col2: { operationType: 'sum' as const }, }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 210716e7494e0..e742b6ba62aff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -735,7 +735,7 @@ describe('IndexPattern Data Source suggestions', () => { sourceField: 'bytes', label: 'Avg of bytes', customLabel: true, - operationType: 'avg', + operationType: 'average', }, }, columnOrder: ['cola', 'colb'], @@ -770,7 +770,7 @@ describe('IndexPattern Data Source suggestions', () => { sourceField: 'bytes', label: 'Avg of bytes', customLabel: true, - operationType: 'avg', + operationType: 'average', }, }, }, @@ -1060,7 +1060,7 @@ describe('IndexPattern Data Source suggestions', () => { customLabel: true, dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, ref: { @@ -1120,7 +1120,7 @@ describe('IndexPattern Data Source suggestions', () => { customLabel: true, dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, ref: { @@ -1468,7 +1468,7 @@ describe('IndexPattern Data Source suggestions', () => { customLabel: true, dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', scale: 'ratio', }, @@ -1537,7 +1537,7 @@ describe('IndexPattern Data Source suggestions', () => { customLabel: true, dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', scale: 'ratio', }, @@ -1601,7 +1601,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, sourceField: 'dest', label: 'Unique count of dest', - operationType: 'cardinality', + operationType: 'unique_count', }, colb: { label: 'My Op', @@ -1647,7 +1647,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, sourceField: 'dest', label: 'Unique count of dest', - operationType: 'cardinality', + operationType: 'unique_count', }, colb: { label: 'My Custom Range', @@ -1723,7 +1723,7 @@ describe('IndexPattern Data Source suggestions', () => { customLabel: true, dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', scale: 'ratio', }, @@ -1843,7 +1843,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'field4', }, col5: { @@ -1951,7 +1951,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'field1', }, }, @@ -2031,7 +2031,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'field1', }, }, @@ -2091,7 +2091,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 381fa4ca27a49..a7c1074ed4eef 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -165,7 +165,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, - operationType: 'avg', + operationType: 'average', sourceField: 'memory', }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index f4fa8bd185b6d..a68f8ae310f3e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -145,7 +145,7 @@ const indexPattern2 = ({ agg: 'histogram', interval: 1000, }, - avg: { + average: { agg: 'avg', }, max: { @@ -569,7 +569,7 @@ describe('loader', () => { dataType: 'number', isBucketed: false, label: '', - operationType: 'avg', + operationType: 'average', sourceField: 'myfield', }, }, @@ -582,7 +582,7 @@ describe('loader', () => { dataType: 'number', isBucketed: false, label: '', - operationType: 'avg', + operationType: 'average', sourceField: 'myfield2', }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 7052a69ee6fb7..ec7ef6a37a27a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -17,7 +17,7 @@ import { IndexPatternField, IndexPatternLayer, } from './types'; -import { updateLayerIndexPattern } from './operations'; +import { updateLayerIndexPattern, translateToOperationName } from './operations'; import { DateRange, ExistingFields } from '../../common/types'; import { BASE_API_URL } from '../../common'; import { @@ -109,7 +109,7 @@ export async function loadIndexPatterns({ const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; if (restriction) { - restrictionsObj[agg] = restriction; + restrictionsObj[translateToOperationName(agg)] = restriction; } }); if (Object.keys(restrictionsObj).length) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx similarity index 95% rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index 5fb0bc3a83528..c50e9270eaac1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/derivative.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -19,6 +19,8 @@ import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { OperationDefinition } from '..'; import { getFormatFromPreviousColumn } from '../helpers'; +const OPERATION_NAME = 'differences'; + const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.derivativeOf', { defaultMessage: 'Differences of {name}', @@ -34,14 +36,14 @@ const ofName = buildLabelFunction((name?: string) => { export type DerivativeIndexPatternColumn = FormattedIndexPatternColumn & ReferenceBasedIndexPatternColumn & { - operationType: 'derivative'; + operationType: typeof OPERATION_NAME; }; export const derivativeOperation: OperationDefinition< DerivativeIndexPatternColumn, 'fullReference' > = { - type: 'derivative', + type: OPERATION_NAME, priority: 1, displayName: i18n.translate('xpack.lens.indexPattern.derivative', { defaultMessage: 'Differences', @@ -78,7 +80,7 @@ export const derivativeOperation: OperationDefinition< previousColumn?.timeScale ), dataType: 'number', - operationType: 'derivative', + operationType: OPERATION_NAME, isBucketed: false, scale: 'ratio', references: referenceIds, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts index f261a0e1e2005..815acb8c4169f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/index.ts @@ -7,5 +7,5 @@ export { counterRateOperation, CounterRateIndexPatternColumn } from './counter_rate'; export { cumulativeSumOperation, CumulativeSumIndexPatternColumn } from './cumulative_sum'; -export { derivativeOperation, DerivativeIndexPatternColumn } from './derivative'; +export { derivativeOperation, DerivativeIndexPatternColumn } from './differences'; export { movingAverageOperation, MovingAverageIndexPatternColumn } from './moving_average'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 513bac14af7a3..fa1691ba9a78e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -25,7 +25,7 @@ const supportedTypes = new Set([ ]); const SCALE = 'ratio'; -const OPERATION_TYPE = 'cardinality'; +const OPERATION_TYPE = 'unique_count'; const IS_BUCKETED = false; function ofName(name: string) { @@ -40,7 +40,7 @@ function ofName(name: string) { export interface CardinalityIndexPatternColumn extends FormattedIndexPatternColumn, FieldBasedIndexPatternColumn { - operationType: 'cardinality'; + operationType: typeof OPERATION_TYPE; } export const cardinalityOperation: OperationDefinition = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 03f8375409246..bf563b877ef5e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -26,6 +26,7 @@ import { buildExpressionFunction } from '../../../../../../../../src/plugins/exp import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components'; const generateId = htmlIdGenerator(); +const OPERATION_NAME = 'filters'; // references types from src/plugins/data/common/search/aggs/buckets/filters.ts export interface Filter { @@ -70,14 +71,14 @@ export const isQueryValid = (input: Query, indexPattern: IndexPattern) => { }; export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn { - operationType: 'filters'; + operationType: typeof OPERATION_NAME; params: { filters: Filter[]; }; } export const filtersOperation: OperationDefinition = { - type: 'filters', + type: OPERATION_NAME, displayName: filtersLabel, priority: 3, // Higher than any metric input: 'none', @@ -86,7 +87,7 @@ export const filtersOperation: OperationDefinition filtersLabel, buildColumn({ previousColumn }) { let params = { filters: [defaultFilter] }; - if (previousColumn?.operationType === 'terms') { + if (previousColumn?.operationType === 'terms' && 'sourceField' in previousColumn) { params = { filters: [ { @@ -103,7 +104,7 @@ export const filtersOperation: OperationDefinition { dataType: 'number', isBucketed: false, label: 'Foo', - operationType: 'avg', // <= invalid + operationType: 'average', // <= invalid sourceField: 'timestamp', }, createMockedIndexPattern() @@ -46,7 +46,7 @@ describe('helpers', () => { dataType: 'number', isBucketed: false, label: 'Foo', - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, createMockedIndexPattern() diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index cdb93048c9a58..b3aa93b062eb1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -437,3 +437,11 @@ export const operationDefinitionMap: Record< (definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }), {} ); + +/** + * Cannot map the prev names, but can guarantee the new names are matching up using the type system + */ +export const renameOperationsMapping: Record = { + avg: 'average', + cardinality: 'unique_count', +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 1767b76e88202..20580634d12e6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -27,7 +27,7 @@ type MetricColumn = FormattedIndexPatternColumn & const typeToFn: Record = { min: 'aggMin', max: 'aggMax', - avg: 'aggAvg', + average: 'aggAvg', sum: 'aggSum', median: 'aggMedian', }; @@ -76,7 +76,6 @@ function buildMetricOperation>({ }, isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.getFieldByName(column.sourceField); - return Boolean( newField && supportedTypes.includes(newField.type) && @@ -124,7 +123,7 @@ function buildMetricOperation>({ } export type SumIndexPatternColumn = MetricColumn<'sum'>; -export type AvgIndexPatternColumn = MetricColumn<'avg'>; +export type AvgIndexPatternColumn = MetricColumn<'average'>; export type MinIndexPatternColumn = MetricColumn<'min'>; export type MaxIndexPatternColumn = MetricColumn<'max'>; export type MedianIndexPatternColumn = MetricColumn<'median'>; @@ -154,7 +153,7 @@ export const maxOperation = buildMetricOperation({ }); export const averageOperation = buildMetricOperation({ - type: 'avg', + type: 'average', priority: 2, displayName: i18n.translate('xpack.lens.indexPattern.avg', { defaultMessage: 'Average', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index a31cf9f019480..639b9e3a95c47 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -74,7 +74,10 @@ export const percentileOperation: OperationDefinition { const existingPercentileParam = - previousColumn?.operationType === 'percentile' && previousColumn?.params.percentile; + previousColumn?.operationType === 'percentile' && + previousColumn.params && + 'percentile' in previousColumn.params && + previousColumn.params.percentile; const newPercentileParam = existingPercentileParam || DEFAULT_PERCENTILE_VALUE; return { label: ofName(getSafeName(field.name, indexPattern), newPercentileParam), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 3b0cb67cbce41..a4a061db04797 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, @@ -13,9 +13,7 @@ import { EuiSwitch, EuiSwitchEvent, EuiSpacer, - EuiPopover, - EuiButtonEmpty, - EuiText, + EuiAccordion, EuiIconTip, } from '@elastic/eui'; import { AggFunctionsMapping } from '../../../../../../../../src/plugins/data/public'; @@ -24,7 +22,7 @@ import { updateColumnParam, isReferenced } from '../../layer_helpers'; import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; -import { ValuesRangeInput } from './values_range_input'; +import { ValuesInput } from './values_input'; import { getEsAggsSuffix, getInvalidFieldMessage } from '../helpers'; import type { IndexPatternLayer } from '../../../types'; @@ -193,8 +191,6 @@ export const termsOperation: OperationDefinition - { updateLayer( @@ -251,71 +247,6 @@ export const termsOperation: OperationDefinition - {!hasRestrictions && ( - - { - setPopoverOpen(!popoverOpen); - }} - > - {i18n.translate('xpack.lens.indexPattern.terms.advancedSettings', { - defaultMessage: 'Advanced', - })} - - } - isOpen={popoverOpen} - closePopover={() => { - setPopoverOpen(false); - }} - > - - updateLayer( - updateColumnParam({ - layer, - columnId, - paramName: 'otherBucket', - value: e.target.checked, - }) - ) - } - /> - - - updateLayer( - updateColumnParam({ - layer, - columnId, - paramName: 'missingBucket', - value: e.target.checked, - }) - ) - } - /> - - - - )} @@ -415,6 +346,57 @@ export const termsOperation: OperationDefinition + {!hasRestrictions && ( + <> + + + + + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'otherBucket', + value: e.target.checked, + }) + ) + } + /> + + + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'missingBucket', + value: e.target.checked, + }) + ) + } + /> + + + )} ); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 0ed611e9726ef..97b57dee2fde7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; -import { EuiRange, EuiSelect, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { EuiFieldNumber, EuiSelect, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import type { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../../mocks'; -import { ValuesRangeInput } from './values_range_input'; +import { ValuesInput } from './values_input'; import type { TermsIndexPatternColumn } from '.'; import { termsOperation } from '../index'; import { IndexPattern, IndexPatternLayer } from '../../../types'; @@ -888,7 +888,7 @@ describe('terms', () => { /> ); - expect(instance.find(EuiRange).prop('value')).toEqual('3'); + expect(instance.find(EuiFieldNumber).prop('value')).toEqual('3'); }); it('should update state with the size value', () => { @@ -904,7 +904,7 @@ describe('terms', () => { ); act(() => { - instance.find(ValuesRangeInput).prop('onChange')!(7); + instance.find(ValuesInput).prop('onChange')!(7); }); expect(updateLayerSpy).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx similarity index 50% rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx index 3603188ba30e5..4303695d6e293 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx @@ -8,52 +8,50 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow } from 'enzyme'; -import { EuiRange } from '@elastic/eui'; -import { ValuesRangeInput } from './values_range_input'; +import { EuiFieldNumber } from '@elastic/eui'; +import { ValuesInput } from './values_input'; jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn()); -describe('ValuesRangeInput', () => { - it('should render EuiRange correctly', () => { +describe('Values', () => { + it('should render EuiFieldNumber correctly', () => { const onChangeSpy = jest.fn(); - const instance = shallow(); + const instance = shallow(); - expect(instance.find(EuiRange).prop('value')).toEqual('5'); + expect(instance.find(EuiFieldNumber).prop('value')).toEqual('5'); }); it('should not run onChange function on mount', () => { const onChangeSpy = jest.fn(); - shallow(); + shallow(); expect(onChangeSpy.mock.calls.length).toBe(0); }); it('should run onChange function on update', () => { const onChangeSpy = jest.fn(); - const instance = shallow(); + const instance = shallow(); act(() => { - instance.find(EuiRange).prop('onChange')!( - { currentTarget: { value: '7' } } as React.ChangeEvent, - true - ); + instance.find(EuiFieldNumber).prop('onChange')!({ + currentTarget: { value: '7' }, + } as React.ChangeEvent); }); - expect(instance.find(EuiRange).prop('value')).toEqual('7'); + expect(instance.find(EuiFieldNumber).prop('value')).toEqual('7'); expect(onChangeSpy.mock.calls.length).toBe(1); expect(onChangeSpy.mock.calls[0][0]).toBe(7); }); it('should not run onChange function on update when value is out of 1-100 range', () => { const onChangeSpy = jest.fn(); - const instance = shallow(); + const instance = shallow(); act(() => { - instance.find(EuiRange).prop('onChange')!( - { currentTarget: { value: '107' } } as React.ChangeEvent, - true - ); + instance.find(EuiFieldNumber).prop('onChange')!({ + currentTarget: { value: '1007' }, + } as React.ChangeEvent); }); instance.update(); - expect(instance.find(EuiRange).prop('value')).toEqual('107'); + expect(instance.find(EuiFieldNumber).prop('value')).toEqual('1007'); expect(onChangeSpy.mock.calls.length).toBe(1); - expect(onChangeSpy.mock.calls[0][0]).toBe(100); + expect(onChangeSpy.mock.calls[0][0]).toBe(1000); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx similarity index 88% rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx index 068e13429527f..915e67c4eba0b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx @@ -7,10 +7,10 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiRange } from '@elastic/eui'; +import { EuiFieldNumber } from '@elastic/eui'; import { useDebounceWithOptions } from '../helpers'; -export const ValuesRangeInput = ({ +export const ValuesInput = ({ value, onChange, }: { @@ -18,7 +18,7 @@ export const ValuesRangeInput = ({ onChange: (value: number) => void; }) => { const MIN_NUMBER_OF_VALUES = 1; - const MAX_NUMBER_OF_VALUES = 100; + const MAX_NUMBER_OF_VALUES = 1000; const [inputValue, setInputValue] = useState(String(value)); @@ -36,13 +36,11 @@ export const ValuesRangeInput = ({ ); return ( - setInputValue(currentTarget.value)} aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 4a05e6f372d30..62cce21ead636 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -120,7 +120,7 @@ describe('state_helpers', () => { isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -147,7 +147,7 @@ describe('state_helpers', () => { isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, }, @@ -365,7 +365,7 @@ describe('state_helpers', () => { isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, col2: { @@ -533,7 +533,7 @@ describe('state_helpers', () => { isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, col2: { @@ -840,7 +840,7 @@ describe('state_helpers', () => { }, indexPattern, columnId: 'col2', - op: 'avg', + op: 'average', field: indexPattern.fields[2], // bytes field visualizationGroups: [], }); @@ -856,7 +856,7 @@ describe('state_helpers', () => { dataType: 'number', isBucketed: false, sourceField: 'bytes', - operationType: 'avg', + operationType: 'average', }), }, incompleteColumns: {}, @@ -1027,7 +1027,7 @@ describe('state_helpers', () => { dataType: 'number' as const, isBucketed: false, sourceField: 'bytes', - operationType: 'avg' as const, + operationType: 'average' as const, }, }, }; @@ -1056,7 +1056,7 @@ describe('state_helpers', () => { { input: ['field'], validateMetadata: () => true, - specificOperations: ['cardinality', 'sum', 'avg'], // this order is ignored + specificOperations: ['unique_count', 'sum', 'average'], // this order is ignored }, ]; const layer: IndexPatternLayer = { @@ -1083,7 +1083,7 @@ describe('state_helpers', () => { expect(result.columnOrder).toEqual(['id1', 'col1']); expect(result.columns).toEqual({ id1: expect.objectContaining({ - operationType: 'avg', + operationType: 'average', }), col1: expect.objectContaining({ operationType: 'testReference', @@ -1097,7 +1097,7 @@ describe('state_helpers', () => { { input: ['field'], validateMetadata: () => true, - specificOperations: ['cardinality'], + specificOperations: ['unique_count'], }, ]; const layer: IndexPatternLayer = { @@ -1122,7 +1122,7 @@ describe('state_helpers', () => { }); expect(result.incompleteColumns).toEqual({ - id1: { operationType: 'cardinality' }, + id1: { operationType: 'unique_count' }, }); expect(result.columns).toEqual({ col1: expect.objectContaining({ @@ -1347,7 +1347,7 @@ describe('state_helpers', () => { columns: { id1: expect.objectContaining({ sourceField: 'timestamp', - operationType: 'cardinality', + operationType: 'unique_count', }), output: expect.objectContaining({ references: ['id1'] }), }, @@ -1463,7 +1463,7 @@ describe('state_helpers', () => { dataType: 'number' as const, isBucketed: false, sourceField: 'bytes', - operationType: 'avg' as const, + operationType: 'average' as const, }; const layer: IndexPatternLayer = { @@ -1475,7 +1475,7 @@ describe('state_helpers', () => { label: 'Reference', dataType: 'number', isBucketed: false, - operationType: 'derivative', + operationType: 'differences', references: ['metric'], }, }, @@ -1829,7 +1829,7 @@ describe('state_helpers', () => { dataType: 'number', isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }; @@ -1915,7 +1915,7 @@ describe('state_helpers', () => { isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, col3: { @@ -1963,7 +1963,7 @@ describe('state_helpers', () => { isBucketed: false, // Private - operationType: 'avg', + operationType: 'average', sourceField: 'bytes', }, ref2: { @@ -2006,7 +2006,7 @@ describe('state_helpers', () => { searchable: true, type: 'number', aggregationRestrictions: { - avg: { + average: { agg: 'avg', }, }, @@ -2076,7 +2076,7 @@ describe('state_helpers', () => { dataType: 'number', isBucketed: false, label: '', - operationType: 'avg', + operationType: 'average', sourceField: 'xxx', }, }, @@ -2107,7 +2107,7 @@ describe('state_helpers', () => { dataType: 'number', isBucketed: false, label: '', - operationType: 'avg', + operationType: 'average', sourceField: 'fieldB', }, }, @@ -2170,7 +2170,7 @@ describe('state_helpers', () => { dataType: 'number', isBucketed: false, label: '', - operationType: 'avg', + operationType: 'average', sourceField: 'fieldD', }, }, @@ -2220,14 +2220,14 @@ describe('state_helpers', () => { describe('getErrorMessages', () => { it('should collect errors from metric-type operation definitions', () => { const mock = jest.fn().mockReturnValue(['error 1']); - operationDefinitionMap.avg.getErrorMessage = mock; + operationDefinitionMap.average.getErrorMessage = mock; const errors = getErrorMessages( { indexPatternId: '1', columnOrder: [], columns: { // @ts-expect-error invalid column - col1: { operationType: 'avg' }, + col1: { operationType: 'average' }, }, }, indexPattern diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 8c5dee8bbb28f..4c54b777b66f3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -56,7 +56,7 @@ describe('getOperationTypesForField', () => { aggregatable: true, searchable: true, }) - ).toEqual(['terms', 'cardinality', 'last_value']); + ).toEqual(['terms', 'unique_count', 'last_value']); }); it('should return only bucketed operations on strings when passed proper filterOperations function', () => { @@ -87,11 +87,11 @@ describe('getOperationTypesForField', () => { 'range', 'terms', 'median', - 'avg', + 'average', 'sum', 'min', 'max', - 'cardinality', + 'unique_count', 'percentile', 'last_value', ]); @@ -109,7 +109,16 @@ describe('getOperationTypesForField', () => { }, (op) => !op.isBucketed ) - ).toEqual(['median', 'avg', 'sum', 'min', 'max', 'cardinality', 'percentile', 'last_value']); + ).toEqual([ + 'median', + 'average', + 'sum', + 'min', + 'max', + 'unique_count', + 'percentile', + 'last_value', + ]); }); it('should return operations on dates', () => { @@ -286,7 +295,7 @@ describe('getOperationTypesForField', () => { }, Object { "field": "bytes", - "operationType": "avg", + "operationType": "average", "type": "field", }, Object { @@ -303,7 +312,7 @@ describe('getOperationTypesForField', () => { "type": "fullReference", }, Object { - "operationType": "derivative", + "operationType": "differences", "type": "fullReference", }, Object { @@ -322,17 +331,17 @@ describe('getOperationTypesForField', () => { }, Object { "field": "timestamp", - "operationType": "cardinality", + "operationType": "unique_count", "type": "field", }, Object { "field": "bytes", - "operationType": "cardinality", + "operationType": "unique_count", "type": "field", }, Object { "field": "source", - "operationType": "cardinality", + "operationType": "unique_count", "type": "field", }, Object { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 140ebc813f6c1..a45650f9323f9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -12,12 +12,19 @@ import { operationDefinitions, GenericOperationDefinition, OperationType, + renameOperationsMapping, } from './definitions'; import { IndexPattern, IndexPatternField } from '../types'; import { documentField } from '../document_field'; export { operationDefinitionMap } from './definitions'; - +/** + * Map aggregation names from Elasticsearch to Lens names. + * Used when loading indexpatterns to map metadata (i.e. restrictions) + */ +export function translateToOperationName(agg: string): OperationType { + return agg in renameOperationsMapping ? renameOperationsMapping[agg] : (agg as OperationType); +} /** * Returns all available operation types as a list at runtime. * This will be an array of each member of the union type `OperationType` diff --git a/x-pack/plugins/lens/public/mocks.ts b/x-pack/plugins/lens/public/mocks.tsx similarity index 68% rename from x-pack/plugins/lens/public/mocks.ts rename to x-pack/plugins/lens/public/mocks.tsx index 10d3be1d1b57d..743846d81213c 100644 --- a/x-pack/plugins/lens/public/mocks.ts +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -5,15 +5,20 @@ * 2.0. */ +import React from 'react'; import { LensPublicStart } from '.'; +import { visualizationTypes } from './xy_visualization/types'; export type Start = jest.Mocked; const createStartContract = (): Start => { const startContract: Start = { - EmbeddableComponent: jest.fn(() => null), + EmbeddableComponent: jest.fn(() => { + return Lens Embeddable Component; + }), canUseEditor: jest.fn(() => true), navigateToPrefilledEditor: jest.fn(), + getXyVisTypes: jest.fn().mockReturnValue(new Promise(() => visualizationTypes)), }; return startContract; }; diff --git a/x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx b/x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx index 34619ae59ae5f..8796f619277ff 100644 --- a/x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx +++ b/x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { render } from 'react-dom'; import { NativeRenderer } from './native_renderer'; import { act } from 'react-dom/test-utils'; @@ -151,4 +151,102 @@ describe('native_renderer', () => { const containerElement: Element = mountpoint.firstElementChild!; expect(containerElement.nodeName).toBe('SPAN'); }); + + it('should properly unmount a react element that is mounted inside the renderer', () => { + let isUnmounted = false; + + function TestComponent() { + useEffect(() => { + return () => { + isUnmounted = true; + }; + }, []); + return <>Hello; + } + + renderAndTriggerHooks( + { + // This render function mimics the most common usage inside Lens + render(, element); + }} + nativeProps={{}} + />, + mountpoint + ); + + // Replaces the component at the mountpoint with nothing + renderAndTriggerHooks(<>Empty, mountpoint); + + expect(isUnmounted).toBe(true); + }); + + it('should call the unmount function provided for non-react elements', () => { + const unmountCallback = jest.fn(); + + renderAndTriggerHooks( + { + return unmountCallback; + }} + nativeProps={{}} + />, + mountpoint + ); + + // Replaces the component at the mountpoint with nothing + renderAndTriggerHooks(<>Empty, mountpoint); + + expect(unmountCallback).toHaveBeenCalled(); + }); + + it('should handle when the mount function is asynchronous without a cleanup fn', () => { + let isUnmounted = false; + + function TestComponent() { + useEffect(() => { + return () => { + isUnmounted = true; + }; + }, []); + return <>Hello; + } + + renderAndTriggerHooks( + { + render(, element); + }} + nativeProps={{}} + />, + mountpoint + ); + + // Replaces the component at the mountpoint with nothing + renderAndTriggerHooks(<>Empty, mountpoint); + + expect(isUnmounted).toBe(true); + }); + + it('should handle when the mount function is asynchronous with a cleanup fn', async () => { + const unmountCallback = jest.fn(); + + renderAndTriggerHooks( + { + return unmountCallback; + }} + nativeProps={{}} + />, + mountpoint + ); + + // Schedule a promise cycle to update the DOM + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Replaces the component at the mountpoint with nothing + renderAndTriggerHooks(<>Empty, mountpoint); + + expect(unmountCallback).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/lens/public/native_renderer/native_renderer.tsx b/x-pack/plugins/lens/public/native_renderer/native_renderer.tsx index 68563e01d7f3f..f0659a130b293 100644 --- a/x-pack/plugins/lens/public/native_renderer/native_renderer.tsx +++ b/x-pack/plugins/lens/public/native_renderer/native_renderer.tsx @@ -5,10 +5,16 @@ * 2.0. */ -import React, { HTMLAttributes } from 'react'; +import React, { HTMLAttributes, useEffect, useRef } from 'react'; +import { unmountComponentAtNode } from 'react-dom'; + +type CleanupCallback = (el: Element) => void; export interface NativeRendererProps extends HTMLAttributes { - render: (domElement: Element, props: T) => void; + render: ( + domElement: Element, + props: T + ) => Promise | CleanupCallback | void; nativeProps: T; tag?: string; } @@ -19,11 +25,42 @@ export interface NativeRendererProps extends HTMLAttributes { * By default the mountpoint element will be a div, this can be changed with the * `tag` prop. * + * If the rendered component tree was using React, we need to clean it up manually, + * otherwise the unmount event never happens. A future addition is for non-React components + * to get cleaned up, which could be added in the future. + * * @param props */ export function NativeRenderer({ render, nativeProps, tag, ...rest }: NativeRendererProps) { + const elementRef = useRef(); + const cleanupRef = useRef<((cleanupElement: Element) => void) | void>(); + useEffect(() => { + return () => { + if (elementRef.current) { + if (cleanupRef.current && typeof cleanupRef.current === 'function') { + cleanupRef.current(elementRef.current); + } + unmountComponentAtNode(elementRef.current); + } + }; + }, []); return React.createElement(tag || 'div', { ...rest, - ref: (el) => el && render(el, nativeProps), + ref: (el) => { + if (el) { + elementRef.current = el; + // Handles the editor frame renderer, which is async + const result = render(el, nativeProps); + if (result instanceof Promise) { + result.then((cleanup) => { + if (typeof cleanup === 'function') { + cleanupRef.current = cleanup; + } + }); + } else if (typeof result === 'function') { + cleanupRef.current = result; + } + } + }, }); } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index fc7e4464624f4..aed4db2e88e21 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -42,7 +42,7 @@ import { VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; -import { EditorFrameStart } from './types'; +import type { EditorFrameStart, VisualizationType } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; import { getSearchProvider } from './search_provider'; @@ -101,6 +101,11 @@ export interface LensPublicStart { * Method which returns true if the user has permission to use Lens as defined by application capabilities. */ canUseEditor: () => boolean; + + /** + * Method which returns xy VisualizationTypes array keeping this async as to not impact page load bundle + */ + getXyVisTypes: () => Promise; } export class LensPlugin { @@ -257,6 +262,10 @@ export class LensPlugin { canUseEditor: () => { return Boolean(core.application.capabilities.visualize?.show); }, + getXyVisTypes: async () => { + const { visualizationTypes } = await import('./xy_visualization/types'); + return visualizationTypes; + }, }; } diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index c0788e6f67dfe..18c73a01cf784 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -15,6 +15,7 @@ const typeToIconMap: { [type: string]: string | IconType } = { labels: 'visText', values: 'number', list: 'list', + visualOptions: 'brush', }; export interface ToolbarPopoverProps { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 6c88eb20826bb..3d34d22c5048a 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -17,7 +17,7 @@ import { Datatable, SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; -import { DragContextState, DragDropIdentifier } from './drag_drop'; +import { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; @@ -64,7 +64,7 @@ export interface EditorFrameProps { showNoDataPopover: () => void; } export interface EditorFrameInstance { - mount: (element: Element, props: EditorFrameProps) => void; + mount: (element: Element, props: EditorFrameProps) => Promise; unmount: () => void; } @@ -142,11 +142,16 @@ export type DropType = | 'field_add' | 'field_replace' | 'reorder' - | 'duplicate_in_group' | 'move_compatible' | 'replace_compatible' | 'move_incompatible' - | 'replace_incompatible'; + | 'replace_incompatible' + | 'replace_duplicate_compatible' + | 'duplicate_compatible' + | 'swap_compatible' + | 'replace_duplicate_incompatible' + | 'duplicate_incompatible' + | 'swap_incompatible'; export interface DatasourceSuggestion { state: T; @@ -185,16 +190,28 @@ export interface Datasource { getLayers: (state: T) => string[]; removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; - renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; - renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; - renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; + renderDataPanel: ( + domElement: Element, + props: DatasourceDataPanelProps + ) => ((cleanupElement: Element) => void) | void; + renderDimensionTrigger: ( + domElement: Element, + props: DatasourceDimensionTriggerProps + ) => ((cleanupElement: Element) => void) | void; + renderDimensionEditor: ( + domElement: Element, + props: DatasourceDimensionEditorProps + ) => ((cleanupElement: Element) => void) | void; + renderLayerPanel: ( + domElement: Element, + props: DatasourceLayerPanelProps + ) => ((cleanupElement: Element) => void) | void; getDropProps: ( props: DatasourceDimensionDropProps & { groupId: string; dragging: DragContextState['dragging']; } - ) => { dropType: DropType; nextLabel?: string } | undefined; + ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { layerId: string; @@ -295,10 +312,11 @@ export interface DatasourceLayerPanelProps { activeData?: Record; } -export interface DraggedOperation { +export interface DraggedOperation extends DraggingIdentifier { layerId: string; groupId: string; columnId: string; + filterOperations: (operation: OperationMetadata) => boolean; } export function isDraggedOperation( @@ -585,12 +603,18 @@ export interface Visualization { * Popover contents that open when the user clicks the contextMenuIcon. This can be used * for extra configurability, such as for styling the legend or axis */ - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; + renderLayerContextMenu?: ( + domElement: Element, + props: VisualizationLayerWidgetProps + ) => ((cleanupElement: Element) => void) | void; /** * Toolbar rendered above the visualization. This is meant to be used to provide chart-level * settings for the visualization. */ - renderToolbar?: (domElement: Element, props: VisualizationToolbarProps) => void; + renderToolbar?: ( + domElement: Element, + props: VisualizationToolbarProps + ) => ((cleanupElement: Element) => void) | void; /** * Visualizations can provide a custom icon which will open a layer-specific popover * If no icon is provided, gear icon is default @@ -620,7 +644,7 @@ export interface Visualization { renderDimensionEditor?: ( domElement: Element, props: VisualizationDimensionEditorProps - ) => void; + ) => ((cleanupElement: Element) => void) | void; /** * The frame will call this function on all visualizations at different times. The diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 982f513ae1019..1130bd7a95d88 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -27,6 +27,9 @@ Object { "type": "expression", }, ], + "curveType": Array [ + "LINEAR", + ], "description": Array [ "", ], diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 0bf5c139e2403..5615a9ac34898 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -24,6 +24,7 @@ import { HorizontalAlignment, ElementClickListener, BrushEndListener, + CurveType, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -179,6 +180,13 @@ export const xyChart: ExpressionFunctionDefinition< help: 'Layers of visual series', multi: true, }, + curveType: { + types: ['string'], + options: ['LINEAR', 'CURVE_MONOTONE_X'], + help: i18n.translate('xpack.lens.xyChart.curveType.help', { + defaultMessage: 'Define how curve type is rendered for a line chart', + }), + }, }, fn(data: LensMultiTable, args: XYArgs) { return { @@ -773,10 +781,17 @@ export function XYChart({ const index = `${layerIndex}-${accessorIndex}`; + const curveType = args.curveType ? CurveType[args.curveType] : undefined; + switch (seriesType) { case 'line': return ( - + ); case 'bar': case 'bar_stacked': @@ -804,11 +819,17 @@ export function XYChart({ key={index} {...seriesProps} fit={isPercentage ? 'zero' : getFitOptions(fittingFunction)} + curve={curveType} /> ); case 'area': return ( - + ); default: return assertNever(seriesType); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 331e27a8efdb0..6a1882edde949 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -148,6 +148,7 @@ export const buildExpression = ( }, ], fittingFunction: [state.fittingFunction || 'None'], + curveType: [state.curveType || 'LINEAR'], axisTitlesVisibilitySettings: [ { type: 'expression', diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 126be41e7b129..6f1a01acd6e76 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -413,8 +413,11 @@ export interface XYArgs { }; tickLabelsVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' }; gridlinesVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_gridlinesConfig' }; + curveType?: XYCurveType; } +export type XYCurveType = 'LINEAR' | 'CURVE_MONOTONE_X'; + // Persisted parts of the state export interface XYState { preferredSeriesType: SeriesType; @@ -428,6 +431,7 @@ export interface XYState { axisTitlesVisibilitySettings?: AxesSettingsConfig; tickLabelsVisibilitySettings?: AxesSettingsConfig; gridlinesVisibilitySettings?: AxesSettingsConfig; + curveType?: XYCurveType; } export type State = XYState; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.test.tsx new file mode 100644 index 0000000000000..c37a36a42fa47 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test/jest'; +import { EuiSwitch } from '@elastic/eui'; +import { LineCurveOption } from './line_curve_option'; + +describe('Line curve option', () => { + it('should show currently selected line curve option', () => { + const component = shallow(); + + expect(component.find(EuiSwitch).prop('checked')).toEqual(true); + }); + + it('should show currently curving disabled', () => { + const component = shallow(); + + expect(component.find(EuiSwitch).prop('checked')).toEqual(false); + }); + + it('should show curving option when enabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsCurveStyleToggle"]')).toEqual(true); + }); + + it('should hide curve option when disabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsCurveStyleToggle"]')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx new file mode 100644 index 0000000000000..ea0a1553ba5e5 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { XYCurveType } from '../types'; + +export interface LineCurveOptionProps { + /** + * Currently selected value + */ + value?: XYCurveType; + /** + * Callback on display option change + */ + onChange: (id: XYCurveType) => void; + isCurveTypeEnabled?: boolean; +} + +export const LineCurveOption: React.FC = ({ + onChange, + value, + isCurveTypeEnabled = true, +}) => { + return isCurveTypeEnabled ? ( + <> + + { + if (e.target.checked) { + onChange('CURVE_MONOTONE_X'); + } else { + onChange('LINEAR'); + } + }} + data-test-subj="lnsCurveStyleToggle" + /> + + + + ) : null; +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_value_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_value_option.test.tsx new file mode 100644 index 0000000000000..851b14839d7f7 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_value_option.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallowWithIntl as shallow, mountWithIntl as mount } from '@kbn/test/jest'; +import { EuiSuperSelect, EuiButtonGroup } from '@elastic/eui'; +import { MissingValuesOptions } from './missing_values_option'; + +describe('Missing values option', () => { + it('should show currently selected fitting function', () => { + const component = shallow( + + ); + + expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); + }); + + it('should show currently selected value labels display setting', () => { + const component = mount( + + ); + + expect(component.find(EuiButtonGroup).prop('idSelected')).toEqual('value_labels_inside'); + }); + + it('should show display field when enabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true); + }); + + it('should hide in display value label option when disabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(false); + }); + + it('should show the fitting option when enabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsMissingValuesSelect"]')).toEqual(true); + }); + + it('should hide the fitting option when disabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsMissingValuesSelect"]')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx new file mode 100644 index 0000000000000..fb6ecec4d2801 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonGroup, EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { FittingFunction, fittingFunctionDefinitions } from '../fitting_functions'; +import { ValueLabelConfig } from '../types'; + +export interface MissingValuesOptionProps { + valueLabels?: ValueLabelConfig; + fittingFunction?: FittingFunction; + onValueLabelChange: (newMode: ValueLabelConfig) => void; + onFittingFnChange: (newMode: FittingFunction) => void; + isValueLabelsEnabled?: boolean; + isFittingEnabled?: boolean; +} + +const valueLabelsOptions: Array<{ + id: string; + value: 'hide' | 'inside' | 'outside'; + label: string; + 'data-test-subj': string; +}> = [ + { + id: `value_labels_hide`, + value: 'hide', + label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.auto', { + defaultMessage: 'Hide', + }), + 'data-test-subj': 'lnsXY_valueLabels_hide', + }, + { + id: `value_labels_inside`, + value: 'inside', + label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.inside', { + defaultMessage: 'Show', + }), + 'data-test-subj': 'lnsXY_valueLabels_inside', + }, +]; + +export const MissingValuesOptions: React.FC = ({ + onValueLabelChange, + onFittingFnChange, + valueLabels, + fittingFunction, + isValueLabelsEnabled = true, + isFittingEnabled = true, +}) => { + const valueLabelsVisibilityMode = valueLabels || 'hide'; + + return ( + <> + {isValueLabelsEnabled && ( + + {i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', { + defaultMessage: 'Labels', + })} + + } + > + value === valueLabelsVisibilityMode)!.id + } + onChange={(modeId) => { + const newMode = valueLabelsOptions.find(({ id }) => id === modeId)!.value; + onValueLabelChange(newMode); + }} + /> + + )} + {isFittingEnabled && ( + + {i18n.translate('xpack.lens.xyChart.missingValuesLabel', { + defaultMessage: 'Missing values', + })}{' '} + + + } + > + { + return { + value: id, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), + inputDisplay: title, + }; + })} + valueOfSelected={fittingFunction || 'None'} + onChange={(value) => onFittingFnChange(value)} + itemLayoutAlign="top" + hasDividers + /> +
+ )} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx new file mode 100644 index 0000000000000..e7ec395312bff --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallowWithIntl as shallow } from '@kbn/test/jest'; +import { Position } from '@elastic/charts'; +import { FramePublicAPI } from '../../types'; +import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks'; +import { State } from '../types'; +import { VisualOptionsPopover } from './visual_options_popover'; +import { ToolbarPopover } from '../../shared_components'; +import { MissingValuesOptions } from './missing_values_option'; + +describe('Visual options popover', () => { + let frame: FramePublicAPI; + + function testState(): State { + return { + legend: { isVisible: true, position: Position.Right }, + valueLabels: 'hide', + preferredSeriesType: 'bar', + layers: [ + { + seriesType: 'bar', + layerId: 'first', + splitAccessor: 'baz', + xAccessor: 'foo', + accessors: ['bar'], + }, + ], + }; + } + + beforeEach(() => { + frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: createMockDatasource('test').publicAPIMock, + }; + }); + it('should disable the visual options for stacked bar charts', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); + }); + + it('should disable the values and fitting for percentage area charts', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(false); + expect(component.find(MissingValuesOptions).prop('isFittingEnabled')).toEqual(false); + }); + + it('should not disable the visual options for percentage area charts', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(false); + }); + + it('should disabled the popover if there is histogram series', () => { + // make it detect an histogram series + frame.datasourceLayers.first.getOperationForColumnId = jest.fn().mockReturnValueOnce({ + isBucketed: true, + scale: 'interval', + }); + const state = testState(); + const component = shallow( + + ); + + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); + }); + + it('should hide the fitting option for bar series', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(MissingValuesOptions).prop('isFittingEnabled')).toEqual(false); + }); + + it('should show the popover and display field enabled for bar and horizontal_bar series', () => { + const state = testState(); + + const component = shallow( + + ); + + expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(true); + }); + + it('should hide in the popover the display option for area and line series', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(false); + }); + + it('should keep the display option for bar series with multiple layers', () => { + frame.datasourceLayers = { + ...frame.datasourceLayers, + second: createMockDatasource('test').publicAPIMock, + }; + + const state = testState(); + const component = shallow( + + ); + + expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(true); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx new file mode 100644 index 0000000000000..fcdef86cc5d0e --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { i18n } from '@kbn/i18n'; +import { ToolbarPopover } from '../../shared_components'; +import { MissingValuesOptions } from './missing_values_option'; +import { LineCurveOption } from './line_curve_option'; +import { XYState } from '../types'; +import { hasHistogramSeries } from '../state_helpers'; +import { ValidLayer } from '../types'; +import { TooltipWrapper } from '../tooltip_wrapper'; +import { FramePublicAPI } from '../../types'; + +function getValueLabelDisableReason({ + isAreaPercentage, + isHistogramSeries, +}: { + isAreaPercentage: boolean; + isHistogramSeries: boolean; +}): string { + if (isHistogramSeries) { + return i18n.translate('xpack.lens.xyChart.valuesHistogramDisabledHelpText', { + defaultMessage: 'This setting cannot be changed on histograms.', + }); + } + if (isAreaPercentage) { + return i18n.translate('xpack.lens.xyChart.valuesPercentageDisabledHelpText', { + defaultMessage: 'This setting cannot be changed on percentage area charts.', + }); + } + return i18n.translate('xpack.lens.xyChart.valuesStackedDisabledHelpText', { + defaultMessage: 'This setting cannot be changed on stacked or percentage bar charts', + }); +} + +export interface VisualOptionsPopoverProps { + state: XYState; + setState: (newState: XYState) => void; + datasourceLayers: FramePublicAPI['datasourceLayers']; +} + +export const VisualOptionsPopover: React.FC = ({ + state, + setState, + datasourceLayers, +}) => { + const isAreaPercentage = state?.layers.some( + ({ seriesType }) => seriesType === 'area_percentage_stacked' + ); + + const hasNonBarSeries = state?.layers.some(({ seriesType }) => + ['area_stacked', 'area', 'line'].includes(seriesType) + ); + + const hasBarNotStacked = state?.layers.some(({ seriesType }) => + ['bar', 'bar_horizontal'].includes(seriesType) + ); + + const isHistogramSeries = Boolean( + hasHistogramSeries(state?.layers as ValidLayer[], datasourceLayers) + ); + + const isValueLabelsEnabled = !hasNonBarSeries && hasBarNotStacked && !isHistogramSeries; + const isFittingEnabled = hasNonBarSeries; + const isCurveTypeEnabled = hasNonBarSeries || isAreaPercentage; + + const valueLabelsDisabledReason = getValueLabelDisableReason({ + isAreaPercentage, + isHistogramSeries, + }); + + const isDisabled = !isValueLabelsEnabled && !isFittingEnabled && !isCurveTypeEnabled; + + return ( + + + { + setState({ + ...state, + curveType: id, + }); + }} + /> + + { + setState({ ...state, valueLabels: newMode }); + }} + onFittingFnChange={(newVal) => { + setState({ ...state, fittingFunction: newVal }); + }} + /> + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 40ac4958aefb9..f965140a48ca0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -7,9 +7,8 @@ import React from 'react'; import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test/jest'; -import { EuiButtonGroupProps, EuiSuperSelect, EuiButtonGroup } from '@elastic/eui'; +import { EuiButtonGroupProps, EuiButtonGroup } from '@elastic/eui'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; -import { ToolbarPopover } from '../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; import { FramePublicAPI } from '../types'; import { State } from './types'; @@ -101,179 +100,6 @@ describe('XY Config panels', () => { }); describe('XyToolbar', () => { - it('should show currently selected fitting function', () => { - const state = testState(); - - const component = shallow( - - ); - - expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); - }); - - it('should show currently selected value labels display setting', () => { - const state = testState(); - - const component = shallow( - - ); - - expect(component.find(EuiButtonGroup).prop('idSelected')).toEqual('value_labels_inside'); - }); - - it('should disable the popover for stacked bar charts', () => { - const state = testState(); - const component = shallow( - - ); - - expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); - }); - - it('should disable the popover for percentage area charts', () => { - const state = testState(); - const component = shallow( - - ); - - expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); - }); - - it('should disabled the popover if there is histogram series', () => { - // make it detect an histogram series - frame.datasourceLayers.first.getOperationForColumnId = jest.fn().mockReturnValueOnce({ - isBucketed: true, - scale: 'interval', - }); - const state = testState(); - const component = shallow( - - ); - - expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); - }); - - it('should show the popover and display field enabled for bar and horizontal_bar series', () => { - const state = testState(); - - const component = shallow( - - ); - - expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true); - }); - - it('should hide the fitting option for bar series', () => { - const state = testState(); - const component = shallow( - - ); - - expect(component.exists('[data-test-subj="lnsMissingValuesSelect"]')).toEqual(false); - }); - - it('should hide in the popover the display option for area and line series', () => { - const state = testState(); - const component = shallow( - - ); - - expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(false); - }); - - it('should keep the display option for bar series with multiple layers', () => { - frame.datasourceLayers = { - ...frame.datasourceLayers, - second: createMockDatasource('test').publicAPIMock, - }; - - const state = testState(); - const component = shallow( - - ); - - expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true); - }); - it('should disable the popover if there is no right axis', () => { const state = testState(); const component = shallow(); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index ac08c55eeadbf..d7868a17bf9db 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -14,15 +14,12 @@ import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, - EuiSuperSelect, EuiFormRow, - EuiText, htmlIdGenerator, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon, - EuiIconTip, } from '@elastic/eui'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { @@ -31,29 +28,17 @@ import { VisualizationDimensionEditorProps, FormatFactory, } from '../types'; -import { - State, - SeriesType, - visualizationTypes, - YAxisMode, - AxesSettingsConfig, - ValidLayer, -} from './types'; -import { - isHorizontalChart, - isHorizontalSeries, - getSeriesColor, - hasHistogramSeries, -} from './state_helpers'; +import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } from './types'; +import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; -import { fittingFunctionDefinitions } from './fitting_functions'; -import { ToolbarPopover, LegendSettingsPopover } from '../shared_components'; +import { LegendSettingsPopover } from '../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; import { TooltipWrapper } from './tooltip_wrapper'; import { getAxesConfiguration } from './axes_configuration'; import { PalettePicker } from '../shared_components'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; import { getSortedAccessors } from './to_expression'; +import { VisualOptionsPopover } from './visual_options_popover/visual_options_popover'; type UnwrapArray = T extends Array ? P : T; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; @@ -92,30 +77,6 @@ const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: }, ]; -const valueLabelsOptions: Array<{ - id: string; - value: 'hide' | 'inside' | 'outside'; - label: string; - 'data-test-subj': string; -}> = [ - { - id: `value_labels_hide`, - value: 'hide', - label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.auto', { - defaultMessage: 'Hide', - }), - 'data-test-subj': 'lnsXY_valueLabels_hide', - }, - { - id: `value_labels_inside`, - value: 'inside', - label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.inside', { - defaultMessage: 'Show', - }), - 'data-test-subj': 'lnsXY_valueLabels_inside', - }, -]; - export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); @@ -159,46 +120,9 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { ); } -function getValueLabelDisableReason({ - isAreaPercentage, - isHistogramSeries, -}: { - isAreaPercentage: boolean; - isHistogramSeries: boolean; -}): string { - if (isHistogramSeries) { - return i18n.translate('xpack.lens.xyChart.valuesHistogramDisabledHelpText', { - defaultMessage: 'This setting cannot be changed on histograms.', - }); - } - if (isAreaPercentage) { - return i18n.translate('xpack.lens.xyChart.valuesPercentageDisabledHelpText', { - defaultMessage: 'This setting cannot be changed on percentage area charts.', - }); - } - return i18n.translate('xpack.lens.xyChart.valuesStackedDisabledHelpText', { - defaultMessage: 'This setting cannot be changed on stacked or percentage bar charts', - }); -} export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps) { const { state, setState, frame } = props; - const hasNonBarSeries = state?.layers.some(({ seriesType }) => - ['area_stacked', 'area', 'line'].includes(seriesType) - ); - - const hasBarNotStacked = state?.layers.some(({ seriesType }) => - ['bar', 'bar_horizontal'].includes(seriesType) - ); - - const isAreaPercentage = state?.layers.some( - ({ seriesType }) => seriesType === 'area_percentage_stacked' - ); - - const isHistogramSeries = Boolean( - hasHistogramSeries(state?.layers as ValidLayer[], frame.datasourceLayers) - ); - const shouldRotate = state?.layers.length ? isHorizontalChart(state.layers) : false; const axisGroups = getAxesConfiguration(state?.layers, shouldRotate); @@ -267,113 +191,15 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp ? 'hide' : 'show'; - const valueLabelsVisibilityMode = state?.valueLabels || 'hide'; - - const isValueLabelsEnabled = !hasNonBarSeries && hasBarNotStacked && !isHistogramSeries; - const isFittingEnabled = hasNonBarSeries; - - const valueLabelsDisabledReason = getValueLabelDisableReason({ - isAreaPercentage, - isHistogramSeries, - }); - return ( - - - {isValueLabelsEnabled ? ( - - {i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', { - defaultMessage: 'Labels', - })} - - } - > - value === valueLabelsVisibilityMode)! - .id - } - onChange={(modeId) => { - const newMode = valueLabelsOptions.find(({ id }) => id === modeId)!.value; - setState({ ...state, valueLabels: newMode }); - }} - /> - - ) : null} - {isFittingEnabled ? ( - - {i18n.translate('xpack.lens.xyChart.missingValuesLabel', { - defaultMessage: 'Missing values', - })}{' '} - - - } - > - { - return { - value: id, - dropdownDisplay: ( - <> - {title} - -

{description}

-
- - ), - inputDisplay: title, - }; - })} - valueOfSelected={state?.fittingFunction || 'None'} - onChange={(value) => setState({ ...state, fittingFunction: value })} - itemLayoutAlign="top" - hasDividers - /> -
- ) : null} -
-
+ { expect(result.attributes.title).toEqual(example.attributes.title); }); }); + + describe('7.13.0 rename operations for Formula', () => { + const context = ({ log: { warning: () => {} } } as unknown) as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '21c145c0-8667-11eb-b6a9-a5bf52bdf519', + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '5ab74ddc-93ca-44e2-9857-ecf85c86b53e': { + columns: { + '2e57a41e-5a52-42d3-877f-bd211d903ef8': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0': { + label: 'Unique count of agent.keyword', + dataType: 'number', + operationType: 'cardinality', + scale: 'ratio', + sourceField: 'agent.keyword', + isBucketed: false, + }, + 'e5efca70-edb5-4d6d-a30a-79384066987e': { + label: 'Average of bytes', + dataType: 'number', + operationType: 'avg', + sourceField: 'bytes', + isBucketed: false, + scale: 'ratio', + }, + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f': { + label: 'Differences of bytes', + dataType: 'number', + operationType: 'derivative', + isBucketed: false, + scale: 'ratio', + references: ['9ca33a9b-f2e6-46ef-a5e1-14bfbe262605'], + }, + '9ca33a9b-f2e6-46ef-a5e1-14bfbe262605': { + label: 'Average of bytes', + dataType: 'number', + operationType: 'avg', + sourceField: 'bytes', + isBucketed: false, + scale: 'ratio', + }, + }, + columnOrder: [ + '2e57a41e-5a52-42d3-877f-bd211d903ef8', + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '9ca33a9b-f2e6-46ef-a5e1-14bfbe262605', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }, + }; + + it('should rename only specific operation types', () => { + const result = migrations['7.13.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + const layers = result.attributes.state.datasourceStates.indexpattern.layers; + expect(layers).toEqual({ + '5ab74ddc-93ca-44e2-9857-ecf85c86b53e': { + columns: { + '2e57a41e-5a52-42d3-877f-bd211d903ef8': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0': { + label: 'Unique count of agent.keyword', + dataType: 'number', + operationType: 'unique_count', + scale: 'ratio', + sourceField: 'agent.keyword', + isBucketed: false, + }, + 'e5efca70-edb5-4d6d-a30a-79384066987e': { + label: 'Average of bytes', + dataType: 'number', + operationType: 'average', + sourceField: 'bytes', + isBucketed: false, + scale: 'ratio', + }, + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f': { + label: 'Differences of bytes', + dataType: 'number', + operationType: 'differences', + isBucketed: false, + scale: 'ratio', + references: ['9ca33a9b-f2e6-46ef-a5e1-14bfbe262605'], + }, + '9ca33a9b-f2e6-46ef-a5e1-14bfbe262605': { + label: 'Average of bytes', + dataType: 'number', + operationType: 'average', + sourceField: 'bytes', + isBucketed: false, + scale: 'ratio', + }, + }, + columnOrder: [ + '2e57a41e-5a52-42d3-877f-bd211d903ef8', + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '9ca33a9b-f2e6-46ef-a5e1-14bfbe262605', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + incompleteColumns: {}, + }, + }); + // should leave other parts alone + expect(result.attributes.state.visualization).toEqual(example.attributes.state.visualization); + expect(result.attributes.state.query).toEqual(example.attributes.state.query); + expect(result.attributes.state.filters).toEqual(example.attributes.state.filters); + expect(result.attributes.title).toEqual(example.attributes.title); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 4c6dfcd7949be..430c1a6caa667 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -106,6 +106,86 @@ interface DatatableStatePost711 { }; } +type OperationTypePre712 = + | 'avg' + | 'cardinality' + | 'derivative' + | 'filters' + | 'terms' + | 'date_histogram' + | 'min' + | 'max' + | 'sum' + | 'median' + | 'percentile' + | 'last_value' + | 'count' + | 'range' + | 'cumulative_sum' + | 'counter_rate' + | 'moving_average'; +type OperationTypePost712 = Exclude< + OperationTypePre712 | 'average' | 'unique_count' | 'differences', + 'avg' | 'cardinality' | 'derivative' +>; +interface LensDocShapePre712 { + visualizationType: string | null; + title: string; + expression: string | null; + state: { + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + layers: Record< + string, + { + columns: Record< + string, + { + operationType: OperationTypePre712; + } + >; + } + >; + }; + }; + visualization: VisualizationState; + query: Query; + filters: Filter[]; + }; +} + +interface LensDocShapePost712 { + visualizationType: string | null; + title: string; + expression: string | null; + state: { + datasourceMetaData: { + filterableIndexPatterns: Array<{ id: string; title: string }>; + }; + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + currentIndexPatternId: string; + layers: Record< + string, + { + columns: Record< + string, + { + operationType: OperationTypePost712; + } + >; + } + >; + }; + }; + visualization: VisualizationState; + query: Query; + filters: Filter[]; + }; +} + /** * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} @@ -387,6 +467,44 @@ const transformTableState: SavedObjectMigrationFn< return newDoc; }; +const renameOperationsForFormula: SavedObjectMigrationFn< + LensDocShapePre712, + LensDocShapePost712 +> = (doc) => { + const renameMapping = { + avg: 'average', + cardinality: 'unique_count', + derivative: 'differences', + } as const; + function shouldBeRenamed(op: OperationTypePre712): op is keyof typeof renameMapping { + return op in renameMapping; + } + const newDoc = cloneDeep(doc); + const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {}; + (newDoc.attributes as LensDocShapePost712).state.datasourceStates.indexpattern.layers = Object.fromEntries( + Object.entries(datasourceLayers).map(([layerId, layer]) => { + return [ + layerId, + { + ...layer, + columns: Object.fromEntries( + Object.entries(layer.columns).map(([columnId, column]) => { + const copy = { + ...column, + operationType: shouldBeRenamed(column.operationType) + ? renameMapping[column.operationType] + : column.operationType, + }; + return [columnId, copy]; + }) + ), + }, + ]; + }) + ); + return newDoc as SavedObjectUnsanitizedDoc; +}; + export const migrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -395,4 +513,5 @@ export const migrations: SavedObjectMigrationMap = { '7.10.0': extractReferences, '7.11.0': removeSuggestedPriority, '7.12.0': transformTableState, + '7.13.0': renameOperationsForFormula, }; diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index 3f3e94099f666..57d8ebf678d61 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -22,7 +22,7 @@ describe('existingFields', () => { } function searchResults(fields: Record = {}) { - return { fields }; + return { fields, _index: '_index', _id: '_id' }; } it('should handle root level fields', () => { @@ -77,7 +77,13 @@ describe('existingFields', () => { it('supports meta fields', () => { const result = existingFields( - [{ _mymeta: 'abc', ...searchResults({ bar: ['scriptvalue'] }) }], + [ + { + // @ts-expect-error _mymeta is not defined on estypes.Hit + _mymeta: 'abc', + ...searchResults({ bar: ['scriptvalue'] }), + }, + ], [field({ name: '_mymeta', isMeta: true })] ); diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 8a2db992a839d..2e6d612835231 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { errors } from '@elastic/elasticsearch'; +import { errors, estypes } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; import { RequestHandlerContext, ElasticsearchClient } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; @@ -192,18 +192,19 @@ async function fetchIndexPatternStats({ _source: false, runtime_mappings: runtimeFields.reduce((acc, field) => { if (!field.runtimeField) return acc; + // @ts-expect-error @elastic/elasticsearch StoredScript.language is required acc[field.name] = field.runtimeField; return acc; - }, {} as Record), + }, {} as Record), script_fields: scriptedFields.reduce((acc, field) => { acc[field.name] = { script: { - lang: field.lang, - source: field.script, + lang: field.lang!, + source: field.script!, }, }; return acc; - }, {} as Record), + }, {} as Record), }, }); return result.hits.hits; @@ -212,10 +213,7 @@ async function fetchIndexPatternStats({ /** * Exported only for unit tests. */ -export function existingFields( - docs: Array<{ fields: Record; [key: string]: unknown }>, - fields: Field[] -): string[] { +export function existingFields(docs: estypes.Hit[], fields: Field[]): string[] { const missingFields = new Set(fields); for (const doc of docs) { @@ -224,7 +222,7 @@ export function existingFields( } missingFields.forEach((field) => { - let fieldStore: Record = doc.fields; + let fieldStore = doc.fields!; if (field.isMeta) { fieldStore = doc; } diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 49ea8c2076f7a..6cddd2c60f416 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { errors } from '@elastic/elasticsearch'; +import { errors, estypes } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; @@ -79,13 +78,14 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }; - const search = async (aggs: unknown) => { + const search = async (aggs: Record) => { const { body: result } = await requestClient.search({ index: indexPattern.title, track_total_hits: true, body: { query, aggs, + // @ts-expect-error @elastic/elasticsearch StoredScript.language is required runtime_mappings: field.runtimeField ? { [fieldName]: field.runtimeField } : {}, }, size: 0, @@ -135,7 +135,7 @@ export async function initFieldsRoute(setup: CoreSetup) { } export async function getNumberHistogram( - aggSearchWithBody: (body: unknown) => Promise, + aggSearchWithBody: (aggs: Record) => Promise, field: IFieldType, useTopHits = true ): Promise { @@ -179,7 +179,10 @@ export async function getNumberHistogram( const terms = 'top_values' in minMaxResult.aggregations!.sample ? minMaxResult.aggregations!.sample.top_values - : { buckets: [] }; + : { + buckets: [] as Array<{ doc_count: number; key: string | number }>, + }; + const topValuesBuckets = { buckets: terms.buckets.map((bucket) => ({ count: bucket.doc_count, @@ -241,7 +244,7 @@ export async function getNumberHistogram( } export async function getStringSamples( - aggSearchWithBody: (body: unknown) => unknown, + aggSearchWithBody: (aggs: Record) => unknown, field: IFieldType ): Promise { const fieldRef = getFieldRef(field); @@ -280,7 +283,7 @@ export async function getStringSamples( // This one is not sampled so that it returns the full date range export async function getDateHistogram( - aggSearchWithBody: (body: unknown) => unknown, + aggSearchWithBody: (aggs: Record) => unknown, field: IFieldType, range: { fromDate: string; toDate: string } ): Promise { diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index d583e1628cbe8..9c9ab7fd0b350 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -137,14 +137,17 @@ export async function getDailyEvents( const byDateByType: Record> = {}; const suggestionsByDate: Record> = {}; + // @ts-expect-error no way to declare aggregations for search response metrics.aggregations!.daily.buckets.forEach((daily) => { const byType: Record = byDateByType[daily.key] || {}; + // @ts-expect-error no way to declare aggregations for search response daily.groups.buckets.regularEvents.names.buckets.forEach((bucket) => { byType[bucket.key] = (bucket.sums.value || 0) + (byType[daily.key] || 0); }); byDateByType[daily.key] = byType; const suggestionsByType: Record = suggestionsByDate[daily.key] || {}; + // @ts-expect-error no way to declare aggregations for search response daily.groups.buckets.suggestionEvents.names.buckets.forEach((bucket) => { suggestionsByType[bucket.key] = (bucket.sums.value || 0) + (suggestionsByType[daily.key] || 0); diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts index 5b084ecfef5e4..3b9bb99caf5b8 100644 --- a/x-pack/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts @@ -50,6 +50,7 @@ export async function getVisualizationCounts( }, }); + // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response const buckets = results.aggregations.groups.buckets; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap index e668929358a6a..268ce4cbb5165 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; -exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index 719fce35a2a68..b75dee3c78306 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

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

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

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

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

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

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

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

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

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

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

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

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

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

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index bc69ab5352a4f..c89d183282219 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -270,7 +270,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem paddingSize="l" >